<pre class='metadata'>
Status: LS-BRANCH
Editor: Pere Lev, pere@pere.life
Favicon: /img/favicon.png
Logo: /img/logo.svg
Status Text: Work in progress
Issue Tracking: Codeberg https://codeberg.org/forgefed/forgefed/issues
Issue Tracker Template: https://codeberg.org/forgefed/forgefed/issues/{0}
Indent: 4
Work Status: exploring
Repository: https://codeberg.org/forgefed/forgefed forgefed
Line Numbers: yes
Markup Shorthands: markdown yes, css no, http no, idl no, markup no
Title: ForgeFed
Shortname: forgefed
URL: https://forgefed.org/spec
Revision: 1
Abstract:
    ForgeFed is a federation protocol for software project hosting and
    collaboration platforms and components of such platforms. It's an extension
    of the ActivityPub protocol.

    This document describes the ForgeFed vocabulary; the rules for
    representing version control and project management related objects as
    linked data; rules for using ActivityPub activities and
    properties to represent forge events; a description the side-effects these
    activities should have; a specification of the activity sequences that
    implement common actions on forge servers.
</pre>

This draft is generated from branch <code>[GITBRANCH]</code>, commit
<a href="https://codeberg.org/ForgeFed/ForgeFed/commit/[GITCOMMIT]">[GITSHORT]</a>.

<!-- -------------------------------------------------------------------------

# #    # ##### #####   ####
# ##   #   #   #    # #    #
# # #  #   #   #    # #    #
# #  # #   #   #####  #    #
# #   ##   #   #   #  #    #
# #    #   #   #    #  ####

-------------------------------------------------------------------------- -->

# Introduction # {#intro}

Note: The spec is still under construction. Want to start implementing? Check
out the ActivityPub implementation guide, and then the [[#s2s]] section here.

Issue: Below are the 3 intro texts from the 3 specs. We probably want to
replace them with a human-friendly tutorial-like example, like in the
ActivityPub spec.

The ForgeFed Vocabulary describes a set of types and properties to be used by
platforms that support the ForgeFed protocol. This specification describes only
the new vocabulary called ForgeFed. The ForgeFed behavior specification
describes how to use this vocabulary, along with standard ActivityPub
vocabulary, to support the ForgeFed protocol.

**The ForgeFed modeling specification** is a set of rules and guidelines which
describe version control repository and project management related objects and
properties, and specify how to represent them as JSON-LD objects (and linked
data in general) using the ForgeFed vocabulary and related vocabularies and
ontologies. Using these modeling rules consistently across implementations and
instances allows to have a common language spoken across networks of software
forges, project management apps and more.

The ForgeFed vocabulary specification defines a dedicated vocabulary of
forge-related terms, and the **modeling specification** uses these terms, along
with terms that already exist in ActivityPub or elsewhere and can be reused for
forge federation.

The ForgeFed behavior specification provides instructions for using Activities,
and which Activities and properties to use, to represent forge events, and
describes the side-effects these Activities should have. The objects used as
inputs and outputs of behavior descriptions there are defined here in the
**modeling specification**.

**The ForgeFed behavior specification** is a set of instructions for
representing version control systems and project management related transactions
using ActivityPub activity objects, and it describes the side effects and
expected results of sending and receiving these activities. The vocabulary for
these activities includes standard ActivityPub terms, new terms defined by
ForgeFed, and terms borrowed from other external vocabularies.

The ForgeFed vocabulary specification defines a dedicated vocabulary of
forge-related terms, and the **behavior specification** uses these terms, along
with terms that already exist in ActivityPub or elsewhere and can be reused for
forge federation.

The ForgeFed modeling specification defines rules for representing forge
related objects as ActivityPub JSON-LD objects, and these objects are used in
the **behavior specification**, included in activities, mentioned in
activities, or modified as a result of activity side-effects.

<!-- -------------------------------------------------------------------------

 ####  #####       # ######  ####  #####  ####
#    # #    #      # #      #    #   #   #
#    # #####       # #####  #        #    ####
#    # #    #      # #      #        #        #
#    # #    # #    # #      #    #   #   #    #
 ####  #####   ####  ######  ####    #    ####

-------------------------------------------------------------------------- -->

# Objects

## Kinds of Objects

Objects are the core concept around which both ActivityPub and ForgeFed are
built. Examples of Objects are [=Note=], [=Ticket=], [=Image=],
[=Create=], [=Push=]. Some objects are resources, which are objects that
contain or represent information and user made or program made content, and
some objects are helpers that exist as implementation detail aren't necessarily
exposed to humans or are useful to humans. But everything is an [=Object=],
represented as compacted JSON-LD.

ForgeFed is an ActivityPub extension, and communication between ForgeFed
implementations occurs using activity objects sent to actor inboxes and
outboxes.

There are 4 kinds of objects in ForgeFed:

: Activities
::  These are objects that describe actions, such as actions that
    happened, actions that are happening, or a request to perform an action.
    Their primary use is for server-to-server interaction between actors by
    being sent to an actor's inbox, and client-to-server interaction between a
    person or program and an actor they control by being sent to the actor's
    outbox. Activities can also appear or be linked inside other objects and
    activities and be listed in Collections.

: Actors
::  These are static persistent objects that have an [=inbox=] and can be
    directly interacted with by POSTing activities to it. Their primary use is
    to contain or represent information and output of user actions or program
    actions, and to manage access to this information and modifications of it.

: Child objects
::  These are persistent objects that, like actors, contain or
    represent information and output of user actions or program actions, but
    they do not have their own [=inbox=] and are not directly interacted with.
    A managed static object always has a parent object, which is an actor, and
    that actor's inbox is the way to interact with the child object. The parent
    actor manages access and modification of the child object.

: Global helper objects
::  These are objects that do not belong to any actor and do not need any
    interaction through activities. As such, they do not exactly fit into the
    actor model, but may be involved in implementation details and practical
    considerations.

Actors, children, and globals are referred to in ForgeFed as *static* objects,
while activities are *dynamic* objects. The terms *constant* and *variable*
are used to indicate whether an object changes during its lifetime or not.

*Static* objects, in addition to being an actor or child or global, also have a
resource/helper distinction:

: Resource
::  Contains or represents information and user made or program made
    content, usually belongs to the domain model of version control systems and
    project management.

: Helper
::  Used for running things behind the scenes, not exposed directly as
    user content, may be transient or auto generated, usually related to
    implementation detail and not to concepts of version control and project
    management.

## Object Publishing and Hosting ## {#publishing}

In ForgeFed, actors host their child objects locally, meaning the actor and the
child object are hosted on the same instance. Actors may create remote objects by
*offering* them to the relevant actor, which then may create the object on their
side and assign it a URI.

The process begins with an [=Offer=] activity, in which:

-   [=object=] MUST be the object being offered for publishing, and that object
    MUST NOT have an [=id=]
-   [=target=] MUST indicate under which list/collection/context the sender would
    like the object to be published (it may also be the URI of the target actor
    itself)

Among the recipients listed in the [=Offer=]'s recipient fields, exactly one
recipient is the actor who is responsible for inspecting and possibly publishing
the newly created object, and possibly sending back an [=Accept=] or a [=Reject=].
We'll refer to this actor as the *target actor*. Specific object types described
throughout this specification have a specific meaning for the *target actor*,
which processing and inspection it is expected to do, and where it is expected
to list the URI of the object once it publishes it.

The sender is essentially asking that the target actor hosts the object as a
child object and assigns is a URI, allowing to observe and interact with the
object. The target actor will be responsible for hosting and controlling the
object, and the sender will just be mentioned as the author.

When an actor *A* receives the [=Offer=] activity, they can determine whether
they're the *target actor* as follows: If the [=target=] is *A* or a child
object of *A*, then *A* is the *target actor*. Otherwise, *A* isn't the target
actor.

In the following example, Luke wants to open a ticket under Aviva's Game Of
Life simulation app:

<div class=example>
<xmp highlight=json-ld>
{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://forgefed.org/ns"
    ],
    "id": "https://forge.example/luke/outbox/02Ljp",
    "type": "Offer",
    "actor": "https://forge.example/luke",
    "to": [
        "https://dev.example/aviva/game-of-life",
        "https://dev.example/aviva/game-of-life/team",
        "https://dev.example/aviva/game-of-life/followers"
    ],
    "object": {
        "type": "Ticket",
        "attributedTo": "https://forge.example/luke",
        "summary": "Test test test",
        "content": "<p>Just testing</p>",
        "mediaType": "text/html",
        "source": {
            "mediaType": "text/markdown; variant=Commonmark",
            "content": "Just testing"
        }
    },
    "target": "https://dev.example/aviva/game-of-life"
}
</xmp>
</div>

The *target actor* SHOULD send an [=Accept=] or a [=Reject=] activity to the
Offer's author in response. If the *target actor* sends an Accept, it MUST
host its own copy, assigning an [=id=] to the newly published object and adding
it to the expected list specified by the [=Offer=]'s [=target=].

If the *target actor* sends a [=Reject=], it MUST NOT add the object's [=id=]
to that list. However if the *target actor* doesn't make any use of the
object, it MAY choose not to send a Reject, e.g. to protect user privacy. The
`Accept` or `Reject` may also be delayed, e.g. until review by a human user;
that is implementation dependent, and implementations should not rely on an
instant response.

In the [=Accept=] activity:

-   [=object=] MUST be the Offer activity or its [=id=]
-   [=result=] MUST be specified and be the [=id=] of the new child object now
    hosted by the *target actor*, which is extracted from the [=Offer=]'s
    [=object=]

In the following example, Luke's ticket is opened automatically and Aviva's
Game Of Life repository, which is an actor, automatically sends Luke an Accept
activity:

<div class=example>
<xmp highlight=json-ld>
{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://forgefed.org/ns"
    ],
    "id": "https://dev.example/aviva/game-of-life/outbox/096al",
    "type": "Accept",
    "actor": "https://dev.example/aviva/game-of-life",
    "to": [
        "https://forge.example/luke",
        "https://dev.example/aviva/game-of-life/team",
        "https://dev.example/aviva/game-of-life/followers"
    ],
    "object": "https://forge.example/luke/outbox/02Ljp",
    "result": "https://dev.example/aviva/game-of-life/issues/113"
}
</xmp>
</div>

<!-- -------------------------------------------------------------------------

        #####
 ####  #     #  ####
#            # #
 ####   #####   ####
     # #            #
#    # #       #    #
 ####  #######  ####

-------------------------------------------------------------------------- -->

# Server to Server Interactions # {#s2s}

Issue: This section describes the whole flow of actor interactions, that allows
the federated implementation of the various features of forges and related
software. It provides a complete picture of interaction flows, which the actor
API section can't, because it focuses on a single actor type at a time.

## Following ## {#following}

Minimal required role: [=visit=]

Following can be done using a [=Follow=] activity, and unfollowing can be done
via an [=Undo=] activity whose [=object=] is a [=Follow=]. And the receiving
actor sends back an Accept or Reject (or nothing). See the ActivityPub
specification section on
[S2S Follow](https://www.w3.org/TR/activitypub/#follow-activity-inbox) for more
details.

However, ForgeFed adds certain elements:

1. The [=Follow=]'s [=object=] is either an actor or an object managed by an
    actor. For example, it's possible to follow a specific [=Ticket=], not just
    the whole [=TicketTracker=]. In that case, the [=TicketTracker=] receives
    the [=Follow=], recognizes that the [=object=] is one of its tickets, and
    responds as normal with Accept/Reject.
2. Following requires the [=visit=] role, i.e. for the [=Follow=] to specify a
    valid [=capability=]. For publicly visible objects (such as public projects
    on typical FOSS code hosting platforms), the [=capability=] MAY be omitted.

## Starring ## {#s2s-star}

On some forges, it's possible to "star" a repository, and repositories display
their count of stars given.

In a federated network, there's a risk of bot servers producing large amount of
stars on a certain repository, making it appear popular, and even misleading
people by serving supposedly-popular exact copies of (really) popular software
projects, except e.g. patched with malware.  Thus, care needs to be taken in
order to avoid such confusion.

Another consideration is that [=Repository=] and [=Project=] and distinct
notions in ForgeFed. Which one would be starred? The repo or the project? What
if a project contains multiple repos? And what if someone wants to star the
project as a whole? Thus, we allow both.

### Add a star ### {#s2s-star-add}

Minimal required role: [=visit=]

Given:

- A [=Project=] or [=Repository=], *R*
- A [=Person=] with [=visit=] access to *R*, Alice

Alice can add her star to *R*'s star count via the following steps:

1. Alice sends *R* a [=Like=] activity in which:
    - [=object=] refers to *R*
    - [=capability=] refers to a [=Grant=] activity that gives the relevant
        access (unless *R* is considered a publicly visible resource and then
        [=capability=] can be omitted)
2. *R* verifies authorization, and that Alice hasn't already [=Like=]d *R*
3. *R* inserts the [=Like=] to its [=likes=] collection and increments the
    collection's [=totalItems=] count
4. If a [=capability=] was specified, *R* SHOULD send Alice an [=Accept=] whose
    [=object=] refers to the [=Like=]. If [=capability=] wasn't specified, *R*
    MAY send such an [=Accept=].

### Remove a star ### {#s2s-star-remove}

Minimal required role: [=visit=]

Given:

- A [=Project=] or [=Repository=], *R*
- A [=Person=] with [=visit=] access to *R*, Alice, who has [=Like=]d *R*

Alice can remove her star from *R*'s star count via the following steps:

1. Alice sends *R* an [=Undo=] activity whose [=object=] is a [=Like=] activity
    in which:
    - [=object=] refers to *R*
    - [=capability=] refers to a [=Grant=] activity that gives the relevant
        access (unless *R* is considered a publicly visible resource and then
        [=capability=] can be omitted)
2. *R* verifies authorization, and that Alice has indeed [=Like=]d *R*
3. *R* removes Alice's past [=Like=] from its [=likes=] collection and
    decrements the collection's [=totalItems=] count
4. If a [=capability=] was specified, *R* SHOULD send Alice an [=Accept=] whose
    [=object=] refers to the [=Undo=]. If [=capability=] wasn't specified, *R*
    MAY send such an [=Accept=].

## Reporting Pushed Commits ## {#pushing}

- Role required for pushing: [=write=]
- Role required for reporting a Push: None (because the [=Repository=] itself
    is publishing the Push)

The ForgeFed [=Push=] activity can be used for representing an action
of pushing commits into a [=Repository=]. Two actors are
involved in the process, the *pusher* (usually a person) and the *repository*,
and they may be hosted on different instances.

[=Push=] activities MUST be authored and published by the [=Repository=], not
by the actor that pushed. That actor is specified in the Push's
[=attributedTo=] property.

Upon a successful push, a ForgeFed implementation that publishes a Push
activity MUST provide the [=type=], [=actor=], [=attributedTo=] and [=target=]
properties as described in [[#Push]].

See example in [[#Push]].

## Issues

### Open ### {#opening-issue}

Minimal required role: [=report=]

The first step for opening a ticket is to determine to which actor to send the
ticket. We'll refer to this actor as the *ticket tracker*. Given an object
*obj* against which you'd like to open a ticket (e.g. some application's source
code repository), look at the [=ticketsTrackedBy=]
property of *obj*.

-   If `ticketsTrackedBy` isn't specified, then *obj* does't declare a way to
    open tickets via ForgeFed.
-   If `ticketsTrackedBy` is specified and is set to the [=id=] of *obj*
    itself, that means *obj* manages its own tickets, i.e. it is the *ticket
    tracker* to which you'll send the ticket.
-   If `ticketsTrackedBy` is specified and is set to some other object, look at
    the [=tracksTicketsFor=] property of that other object.  If the [=id=] of
    *obj* is listed there under `tracksTicketsFor`, then that other object is
    the *ticket tracker* to which you'll send the ticket.  Implementations
    SHOULD verify this bidirectional reference between the object and the
    tracker, and SHOULD NOT send a ticket if the bidirectional reference isn't
    found.

Now that we've determined the *ticket tracker*, i.e. the actor to whom we'll
send the [=Ticket=], the ticket may be opened using an [=Offer=]
activity in which:

-   [=object=] is the ticket to be opened, it's a [=Ticket=] object with fields
    as described in [[#Ticket]]. It MUST specify at least [=attributedTo=],
    [=summary=] and [=content=], and MUST NOT specify [=id=]. If it specifies a
    [=context=], then it MUST be identical the Offer's [=target=] described
    below.
-   [=target=] is the ticket tracker to which the actor is offering the Ticket
    (e.g. a repository or project etc. under which the ticket will be opened if
    accepted). It MUST be either an actor or a child object. If it's a child
    object, the actor to whom the child object belongs MUST be listed as a
    recipient in the Offer's [=to=] field. If it's an actor, then that actor
    MUST be listed in the `to` field.

The *target actor* MAY then send back an Accept or Reject. The action that has
been taken by the *target actor* is indicated to the ticket author as follows:

-   If a [=Reject=] was sent, it means the ticket hasn't been assigned an
    [=id=] URI by the tracker and isn't being tracked by the tracker
-   If an [=Accept=] was sent, it means the ticket is now tracked and hosted on
    the target's side

In the following example, Luke wants to open a ticket under Aviva's Game Of
Life simulation app:

<div class=example>
<xmp highlight=json-ld>
{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://forgefed.org/ns"
    ],
    "id": "https://forge.example/luke/outbox/02Ljp",
    "type": "Offer",
    "actor": "https://forge.example/luke",
    "to": [
        "https://dev.example/aviva/game-of-life",
        "https://dev.example/aviva/game-of-life/team",
        "https://dev.example/aviva/game-of-life/followers"
    ],
    "object": {
        "type": "Ticket",
        "attributedTo": "https://forge.example/luke",
        "summary": "Test test test",
        "content": "<p>Just testing</p>",
        "mediaType": "text/html",
        "source": {
            "mediaType": "text/markdown; variant=Commonmark",
            "content": "Just testing"
        }
    },
    "target": "https://dev.example/aviva/game-of-life"
}
</xmp>
</div>

Luke's ticket is opened automatically and Aviva's Game Of Life repository,
which is an actor, automatically sends Luke an Accept activity:

<div class=example>
<xmp highlight=json-ld>
{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://forgefed.org/ns"
    ],
    "id": "https://dev.example/aviva/game-of-life/outbox/096al",
    "type": "Accept",
    "actor": "https://dev.example/aviva/game-of-life",
    "to": [
        "https://forge.example/luke",
        "https://dev.example/aviva/game-of-life/team",
        "https://dev.example/aviva/game-of-life/followers"
    ],
    "object": "https://forge.example/luke/outbox/02Ljp",
    "result": "https://dev.example/aviva/game-of-life/issues/113"
}
</xmp>
</div>

### Update title and description ### {#updating-issue-text}

See [[#editing-ticket]].

### Update milestone ### {#s2s-issue-milestone}

See [[#s2s-edit-ticket-milestone]].

### Update custom fields ### {#updating-issue-custom}

See [[#editing-ticket-custom]].

### Close ### {#closing-issue}

Minimal required role: [=triage=]

Given:

- A [=TicketTracker=] actor, *T*
- An actor with [=triage=] access to *T*, Alice
- A [=Ticket=] *t*, that is tracked by *T*

If *t* is open, i.e. its [=isResolved=] isn't specified or is set to False, *t*
may be closed via the following steps:

1. Alice sends *T* a [=Resolve=] activity where:
    - [=object=] is the URI of *t*
    - [=capability=] is the URI of a [=Grant=] authorizing the action
2. *T* sets *t*'s [=isResolved=] to True, and MAY set [=resolvedBy=] to Alice
    or to the [=Resolve=], and MAY set [=resolved=] to the current time
3. *T* publishes an [=Accept=] whose [=object=] points to the [=Resolve=]

### Reopen ### {#reopening-issue}

Minimal required role: [=triage=]

Given:

- A [=TicketTracker=] actor, *T*
- An actor with [=triage=] access to *T*, Alice
- A [=Ticket=] *t*, that is tracked by *T*

If *t* is closed, i.e. its [=isResolved=] is True, *t* may be reopened via the
following steps:

1. Alice sends *T* a [=Undo=] activity whose:
    - [=object=] is a [=Resolve=] activity whose [=object=] is the URI of *t*
    - [=capability=] is the URI of a [=Grant=] authorizing the action
2. *T* sets *t*'s [=isResolved=] to False
3. *T* publishes an [=Accept=] whose [=object=] points to the [=Undo=]

### Assign ### {#issue-assign}

Minimal required role: [=triage=]

Given:

- A [=TicketTracker=] actor, *T*
- An actor with [=triage=] access to *T*, Tessa
- A [=Ticket=] *t*, that is tracked by *T*
- A [=Person=] or [=Team=] that is about to be assigned, *A*

*A* can be assigned to *t* via the following steps.

#### Assign myself #### {#issue-assign-self}

If *A* and Tessa are the same actor:

1. Tessa sends *T* an [=Assign=] activity whose:
    - [=object=] is Tessa
    - [=target=] is *t*
    - [=capability=] is the URI of a [=Grant=] authorizing the action
2. *T* inserts to *t*'s [=assignments=] collection a [=Relationship=] object
    with an [=id=], whose [=subject=] is *t*, [=relationship=] is
    [=assignedTo=] and [=object=] is Tessa
3. *T* sends an [=Accept=] whose [=object=] is the Assign and whose [=result=]
    is the ID URI of the new [=Relationship=] object

#### Assign another actor #### {#issue-assign-other}

If *A* and Tessa aren't the same actor:

1. Tessa sends *T* and *A* an [=Assign=] activity whose:
    - [=object=] is *A*
    - [=target=] is *t*
    - [=capability=] is the URI of a [=Grant=] authorizing the action
2. *T* sends *A* an [=Accept=] whose [=object=] is the Assign and whose
    [=result=] isn't specified, to indicate that the Assign is authorized
3. If *A* is a [=Team=], an actor with [=triage=] access to *A* sends an
    [=Accept=] whose:
    - [=object=] is the Assign
    - [=capability=] is the URI of a [=Grant=] for *A*, with role [=triage=] or
        higher
4. *A* publishes an [=Accept=] whose [=object=] is the Assign
5. *T* inserts to *t*'s [=assignments=] collection a [=Relationship=] object
    with an [=id=], whose [=subject=] is *t*, [=relationship=] is
    [=assignedTo=] and [=object=] is *A*
6. *T* sends an [=Accept=] whose [=object=] is the Assign and whose [=result=]
    is the ID URI of the new [=Relationship=] object

### Unassign ### {#issue-unassign}

#### Unassign myself #### {#issue-unassign-self}

Minimal required role: [=visit=]

Given:

- A [=TicketTracker=] actor, *T*
- A [=Ticket=] *t*, that is tracked by *T*
- A [=Person=] assigned to *t*, Tessa

Tessa can be unassigned from *t* via the following steps:

1. Tessa sends *T* and *A* an [=Undo=] activity whose:
    - [=object=] is an object whose:
        - [=type=] is [=Assign=]
        - [=object=] is Tessa
        - [=target=] is *t*
    - [=capability=] is the URI of a [=Grant=] authorizing the action (unless
        *T* is considered a publicly visible resource, in which case the
        [=capability=] can be omitted)
2. *T* removes from *t*'s [=assignments=] collection the [=Relationship=]
    object whose [=object=] is Tessa
3. *T* sends an [=Accept=] whose [=object=] is the Assign and whose [=result=]
    is the ID URI of the removed [=Relationship=] object

#### Unassign another actor #### {#issue-unassign-other}

Minimal required role: [=triage=]

Given:

- A [=TicketTracker=] actor, *T*
- An actor with [=triage=] access to *T*, Tessa
- A [=Ticket=] *t*, that is tracked by *T*
- A [=Person=] or [=Team=] that is assigned to *t*, *A* (who isn't Tessa)

*A* can be unassigned from *t* via the following steps:

1. Tessa sends *T* and *A* an [=Undo=] activity whose:
    - [=object=] is an object whose:
        - [=type=] is [=Assign=]
        - [=object=] is *A*
        - [=target=] is *t*
    - [=capability=] is the URI of a [=Grant=] authorizing the action
2. *T* sends *A* an [=Accept=] whose [=object=] is the Undo and whose
    [=result=] isn't specified, to indicate that the Assign is authorized
3. *A* publishes an [=Accept=] whose [=object=] is the Assign
4. *T* removes from *t*'s [=assignments=] collection the [=Relationship=]
    object whose [=object=] is *A*
5. *T* sends an [=Accept=] whose [=object=] is the Assign and whose [=result=]
    is the ID URI of the removed [=Relationship=] object

### Delete ### {#deleting-issue}

Minimal required role: [=admin=]

- A [=TicketTracker=] actor, *T*
- An actor with [=admin=] access to *T*, Alice
- A [=Ticket=] *t*, that is tracked by *T*

*t* may be deleted via the following steps:

1. Alice sends *T* a [=Delete=] activity whose:
    - [=object=] is the URI of *t*
    - [=capability=] is the URI of a [=Grant=] authorizing the action
2. *T* deletes *t* such that GET requests for *t* get a 404 or 410 response
3. *T* publishes an [=Accept=] whose [=object=] points to the [=Delete=]

## Merge Requests ## {#s2s-mr}

### Open ### {#opening-mr}

Minimal required role: [=report=]

If actor *A* wishes to submit a Merge Request (MR)/Pull Request (PR)/patch
against a [=Repository=] *R*, it may do so by following these
steps:

1.  Look at *R*'s [=sendPatchesTo=] property: That is the [=PatchTracker=] to
    which the MR needs to be submitted; let's call it *P*
2.  Verify that *P* consents to handling MRs for repository *R* by verifying
    that *R* is listed in *P*'s [=tracksPatchesFor=] property
3.  Publish and deliver, at least to *P*, an [=Offer=] activity in which:
    - [=actor=] is *A*
    - [=target=] is *P*
    - [=object=] is a [=Ticket=] in which:
        * [=id=] isn't specified
        * [=type=] is [=Ticket=]
        * [=attributedTo=] is *A*
        * [=summary=] is a one-line HTML-escaped plain-text title of the MR
        * [=source=] is the MR's description
        * [=content=] is an HTML rendering of the MR's description
        * [=context=], if specified, is *P*
        *   Among the [=attachment=]s there's exactly one of type [=Offer=], in
            which:
            +   [=type=] is [=Offer=]
            +   [=origin=] is the [=Repository=] or [=Branch=] from which the
                proposed changes are proposed to be merged into the target
                repository/branch
            +   [=target=] is the [=Repository=] or [=Branch=] into which the
                changes are proposed to be merged
            +   [=object=] is an [=OrderedCollection=] of [=Patch=] objects in
                reverse chronological order, all of them with:
                -   the same [=mediaType=]
                -   that [=mediaType=] MUST match the Version Control System of
                    the target [=Repository=]
                -   [=attributedTo=] MUST be *A*
            +   At least [=origin=] or [=object=] MUST be provided, both MAY be
                provided

Actor *P* MAY send back an [=Accept=] or [=Reject=]. The action that has been
taken by *P* is indicated to actor *A* as follows:

-   If a [=Reject=] was sent, it mean the MR has been rejected, and isn't being
    tracked by *P*
-   If an [=Accept=] was sent, it means the MR is now tracked by *P*, and its
    [=id=] is indicated by the [=Accept=]'s [=result=]

In the following example, Luke wants to open a Merge Request against a Game Of
Life simulation app:

<div class=example>
<xmp highlight=json-ld>
{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://forgefed.org/ns"
    ],
    "id": "https://forge.example/luke/outbox/uCSW6urN",
    "type": "Offer",
    "actor": "https://forge.example/luke",
    "to": [
        "https://dev.example/projects/game-of-life/pr-tracker"
    ],
    "cc": [
        "https://dev.example/projects/game-of-life",
        "https://dev.example/projects/game-of-life/followers",
        "https://dev.example/projects/game-of-life/repo",
        "https://dev.example/projects/game-of-life/repo/followers",
        "https://dev.example/projects/game-of-life/pr-tracker/followers"
    ],
    "object": {
        "type": "Ticket",
        "attributedTo": "https://forge.example/luke",
        "summary": "Fix the animation bug",
        "content": "<p>Please review, thanks!</p>",
        "mediaType": "text/html",
        "source": {
            "mediaType": "text/markdown; variant=Commonmark",
            "content": "Please review, thanks!"
        },
        "attachment": {
            "type": "Offer",
            "origin": {
                "type": "Branch",
                "context": "https://forge.example/luke/game-of-life",
                "ref": "refs/heads/fix-animation-bug"
            },
            "target": {
                "type": "Branch",
                "context": "https://dev.example/projects/game-of-life/repo",
                "ref": "refs/heads/main"
            },
            "object": {
                "type": "OrderedCollection",
                "totalItems": 1,
                "items": [
                    {
                        "type": "Patch",
                        "attributedTo": "https://forge.example/luke",
                        "mediaType": "application/x-git-patch",
                        "content": "From c9ae5f4ff4a330b6e1196ceb7db1665bd4c1..."
                    }
                ]
            }
        }
    },
    "target": "https://dev.example/projects/game-of-life/pr-tracker"
}
</xmp>
</div>

Luke's MR is opened automatically and the [=PatchTracker=]
sends Luke an Accept activity:

<div class=example>
<xmp highlight=json-ld>
{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://forgefed.org/ns"
    ],
    "id": "https://dev.example/projects/game-of-life/pr-tracker/outbox/qQfFKwJ8",
    "type": "Accept",
    "actor": "https://dev.example/projects/game-of-life/pr-tracker",
    "to": [
        "https://forge.example/luke"
    ],
    "cc": [
        "https://dev.example/projects/game-of-life",
        "https://dev.example/projects/game-of-life/followers",
        "https://dev.example/projects/game-of-life/repo",
        "https://dev.example/projects/game-of-life/repo/followers",
        "https://dev.example/projects/game-of-life/pr-tracker/followers"
    ],
    "object": "https://forge.example/luke/outbox/uCSW6urN",
    "result": "https://dev.example/projects/game-of-life/pr-tracker/pulls/1219"
}
</xmp>
</div>

### Apply ### {#applying-mr}

Minimal required role: [=write=]

Given:

- A [=Repository=] actor, *R*
- A [=PatchTracker=] actor, *P*, that is tracking patches for *R*
- An actor with [=write=] access to *P*, Alice
- A [=Ticket=] *t*, that is a Merge Request tracked by *P*

*t* may be merged into repository *R* via the following steps:

1. Alice sends *P* an [=Apply=] activity where:
    - [=object=] is the URI of *t*
    - [=target=] is the URI of *R* or a [=Branch=] object with [=context=]
        being the URI of *R*, [=name=] being the branch name (e.g. "main") and
        [=ref=] being the unique identifier of the branch within the repo e.g.
        "refs/heads/main")
    - [=capability=] is the URI of a [=Grant=] activity granting Alice
        [=write=] access to *P*
2. *P* verifies that:
    - The [=Apply=]'s [=target=] matches the target repo/branch specified by
        *t*
    - *t* can be merged without conflict
    - The specified [=capability=] is valid
3. *P*, via the version control system's protocol, pushes the patches/commits
    to *R*
4. *P* publishes an [=Accept=] activity whose [=object=] points to the
    [=Apply=], and marks *t* as resolved
5. *R*, upon processing the push, publishes a [=Push=] activity, see
    [[#pushing]].

### Update title and description ### {#updating-mr-text}

See [[#editing-ticket]].

### Update milestone ### {#s2s-mr-milestone}

See [[#s2s-edit-ticket-milestone]].

### Update custom fields ### {#updating-mr-custom}

See [[#editing-ticket-custom]].

### Update patches (via web) ### {#updating-mr-patches}

Minimal required role:

- For the MR author: [=report=]
- Other actors: [=write=]

Given:

- A [=Repository=] actor, *R*
- A [=PatchTracker=] actor, *P*, that is tracking patches for *R*
- An actor with [=write=] access to *P*, Alice
- A [=Ticket=] *t*, that is a Merge Request tracked by *P*
- *t*'s [=attachment=] is an [=Offer=] whose [=object=] is specified and is an
    [=OrderedCollection=], in reverse chronological/logical order, of one or
    more [=Patch=] objects

*t* may receive an updated version of its patch set via the following steps:

1. Alice sends *P* an [=Add=] activity, where:
    - [=object=] is an [=OrderedCollection=], in reverse chronological/logical
        order, or one or more [=Patch=] objects, whose [=mediaType=] matches
        the VCS of *R* (e.g. Git patches for a Git repo)
    - [=target=] is the URI of *t*
    - [=capability=] is the URI of a [=Grant=] that authorizes this action
2. *P* updates the [=OrderedCollection=] of [=Patch=] objects under *t*:
    - The current collection is placed at the top of the [=previousVersions=]
        list
    - The new collection takes the place of the current one, and receives an
        [=id=] field, i.e. a URI
    - The [=currentVersion=] fields in all [=previousVersions=] items are
        updated to specify that newly assigned [=id=]
3. *P* publishes an [=Accept=] where:
    - [=object=] is the URI of the [=Add=]
    - [=result=] is the newly assigned URI of the new patch collection

For example, here's what *t* might look like:

<div class=example>
<xmp highlight=json-ld line-highlight=23>
{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://forgefed.org/ns"
    ],
    "id": "https://fig.fr33domlover.site/looms/9nOkn/cloths/mbWob",
    "type": "Ticket",
    "context": "https://fig.fr33domlover.site/looms/9nOkn",
    "attributedTo": "https://walnut.fr33domlover.site/people/gK2eJ",
    "isResolved": false,
    "published": "2022-09-28T16:03:54.327751Z",

    "summary": "Fix the bug",
    "source": {
        "content": "One line of code was enough!",
        "mediaType": "text/markdown; variant=Commonmark"
    },
    "mediaType": "text/html",
    "content": "<p>One line of code was enough!</p>",

    "attachment": {
        "type": "Offer",
        "object": "https://fig.fr33domlover.site/looms/9nOkn/cloths/mbWob/bundles/mbWob",
        "origin": {
            "@context": [
                "https://www.w3.org/ns/activitystreams",
                "https://forgefed.org/ns"
            ],
            "context": "https://grape.fr33domlover.site/repos/9GnzG",
            "name": "main",
            "ref": "refs/heads/main",
            "type": "Branch"
        },
        "target": {
            "@context": [
                "https://www.w3.org/ns/activitystreams",
                "https://forgefed.org/ns"
            ],
            "context": "https://fig.fr33domlover.site/repos/9nOkn",
            "name": "main",
            "ref": "refs/heads/main",
            "type": "Branch"
        }
    },

    "followers": "https://fig.fr33domlover.site/looms/9nOkn/cloths/mbWob/followers",
    "replies": "https://fig.fr33domlover.site/looms/9nOkn/cloths/mbWob/discussion"
}
</xmp>
</div>

And the collection object pointed by [=attachment=]'s [=object=] is:

<div class=example>
<xmp highlight=json-ld>
{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://forgefed.org/ns"
    ],
    "id": "https://fig.fr33domlover.site/looms/9nOkn/cloths/mbWob/bundles/mbWob",
    "type": "OrderedCollection",
    "context": "https://fig.fr33domlover.site/looms/9nOkn/cloths/mbWob",
    "orderedItems": [
        "https://fig.fr33domlover.site/looms/9nOkn/cloths/mbWob/bundles/mbWob/patches/4nZwD"
    ],
    "previousVersions": [
        "https://fig.fr33domlover.site/looms/9nOkn/cloths/mbWob/bundles/J7f4T",
        "https://fig.fr33domlover.site/looms/9nOkn/cloths/mbWob/bundles/m6F29"
    ],
    "totalItems": 1
}
</xmp>
</div>

Note that [=context=] points back to *t* and the item under [=orderedItems=]
points to a [=Patch=] object, i.e. it's a MR with a single patch. And there are
already 2 older versions.

After processing the [=Add=], *t* now looks like this - the only change here is
in line 23 which points to the new collection now:

<div class=example>
<xmp highlight=json-ld line-highlight=23>
{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://forgefed.org/ns"
    ],
    "id": "https://fig.fr33domlover.site/looms/9nOkn/cloths/mbWob",
    "type": "Ticket",
    "context": "https://fig.fr33domlover.site/looms/9nOkn",
    "attributedTo": "https://walnut.fr33domlover.site/people/gK2eJ",
    "isResolved": false,
    "published": "2022-09-28T16:03:54.327751Z",

    "summary": "Fix the bug",
    "source": {
        "content": "One line of code was enough!",
        "mediaType": "text/markdown; variant=Commonmark"
    },
    "mediaType": "text/html",
    "content": "<p>One line of code was enough!</p>",

    "attachment": {
        "type": "Offer",
        "object": "https://fig.fr33domlover.site/looms/9nOkn/cloths/mbWob/bundles/5Km3d",
        "origin": {
            "@context": [
                "https://www.w3.org/ns/activitystreams",
                "https://forgefed.org/ns"
            ],
            "context": "https://grape.fr33domlover.site/repos/9GnzG",
            "name": "main",
            "ref": "refs/heads/main",
            "type": "Branch"
        },
        "target": {
            "@context": [
                "https://www.w3.org/ns/activitystreams",
                "https://forgefed.org/ns"
            ],
            "context": "https://fig.fr33domlover.site/repos/9nOkn",
            "name": "main",
            "ref": "refs/heads/main",
            "type": "Branch"
        }
    },

    "followers": "https://fig.fr33domlover.site/looms/9nOkn/cloths/mbWob/followers",
    "replies": "https://fig.fr33domlover.site/looms/9nOkn/cloths/mbWob/discussion"
}
</xmp>
</div>

That new collection looks like this:

<div class=example>
<xmp highlight=json-ld>
{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://forgefed.org/ns"
    ],
    "id": "https://fig.fr33domlover.site/looms/9nOkn/cloths/mbWob/bundles/5Km3d",
    "type": "OrderedCollection",
    "context": "https://fig.fr33domlover.site/looms/9nOkn/cloths/mbWob",
    "orderedItems": [
        "https://fig.fr33domlover.site/looms/9nOkn/cloths/mbWob/bundles/mbWob/patches/6F94J",
        "https://fig.fr33domlover.site/looms/9nOkn/cloths/mbWob/bundles/mbWob/patches/0P5bE",
        "https://fig.fr33domlover.site/looms/9nOkn/cloths/mbWob/bundles/mbWob/patches/hDJ8r",
        "https://fig.fr33domlover.site/looms/9nOkn/cloths/mbWob/bundles/mbWob/patches/I8Y6c"
    ],
    "previousVersions": [
        "https://fig.fr33domlover.site/looms/9nOkn/cloths/mbWob/bundles/mbWob",
        "https://fig.fr33domlover.site/looms/9nOkn/cloths/mbWob/bundles/J7f4T",
        "https://fig.fr33domlover.site/looms/9nOkn/cloths/mbWob/bundles/m6F29"
    ],
    "totalItems": 4
}
</xmp>
</div>

Note that the new version happens to have 4 patches/commits, and that
[=previousVersions=] now also lists the last version, that we just replaced.

And here's the collection we replaced, which now has [=currentVersion=]
pointing to the new collection (and it doesn't specify [=previousVersions=]
anymore, since that information can be found under the latest collection).

<div class=example>
<xmp highlight=json-ld line-highlight=12>
{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://forgefed.org/ns"
    ],
    "id": "https://fig.fr33domlover.site/looms/9nOkn/cloths/mbWob/bundles/mbWob",
    "type": "OrderedCollection",
    "context": "https://fig.fr33domlover.site/looms/9nOkn/cloths/mbWob",
    "orderedItems": [
        "https://fig.fr33domlover.site/looms/9nOkn/cloths/mbWob/bundles/mbWob/patches/4nZwD"
    ],
    "totalItems": 1,
    "currentVersion": "https://fig.fr33domlover.site/looms/9nOkn/cloths/mbWob/bundles/5Km3d"
}
</xmp>
</div>

### Update patches (via push) ### {#pushing-mr-patches}

Minimal required role:

- For the repo push access: [=write=]

Given:

- A [=Repository=] actor, *R*
- A [=PatchTracker=] actor, *P*, that is tracking patches for *R*
- An actor with [=write=] access to *R*, Luke
- A [=Ticket=] *t*, that is a Merge Request tracked by *P*
- *t*'s [=attachment=] is an [=Offer=] whose [=origin=] is specified and is a
    URI or a [=Repository=], or a [=Branch=] object (depending on whether the
    VCS supports branches, e.g. Git does but Darcs doesn't)

*t* may receive an updated version of its patch set, by having Luke perform a
VCS push to the origin repository/branch. It may be done via the following
steps:

1. Luke pushes (or force-pushes, i.e. adding, changing or deleting commits) to
    the origin repository/branch
2. *P*, which SHOULD be a follower of the origin repository, receives the
    [=Push=] activity and checks if any of its Merge Requests have that
    repository/branch as their origin - indeed *t* is such a Merge Request
3. *P* pulls the origin repo/branch, compares it to the [=Offer=]'s [=target=]
    repo/branch, and updates the [=Offer=]'s [=object=], i.e. the patch
    collection, in the same manner as described in [[#updating-mr-patches]].
    Note that if no [=object=] collection is specified, *P* isn't required to
    generate patches. However, if it's specified, *P* MUST either update it or
    remove it.

### Close ### {#closing-mr}

A Merge Request may be closed without being merged/applied. It is the same
process as for issues, see [[#closing-issue]].

### Reopen ### {#reopening-mr}

A Merge Request that was *closed without being merged/applied* can be reopened.
Aside from that constraint, it is the same process as for issues. See
[[#reopening-issue]].

### Assign ### {#mr-assign}

A Merge Request can be assigned in the same process as for issues. See
[[#issue-assign]].

### Unassign ### {#mr-unassign}

A Merge Request can be unassigned in the same process as for issues. See
[[#issue-unassign]].

### Delete ### {#deleting-mr}

A Merge Request may be deleted. It is the same process as for issues, see
[[#deleting-issue]].

### Review ### {#s2s-mr-review}

Minimal required role: [=report=]

Given:

- A [=PatchTracker=], *T*
- A merge request (i.e. [=Ticket=]) under *T*, *t*
- A [=Person=] with [=report=] access to *T*, Alice

Alice can submit a review on *t* via the following steps:

1. Via *t*'s [=mrDiff=] Alice obtains the changes that *t* introduces
2. Alice publishes [=Note=] objects for the review comments - both the summary
    comment, and a top comment for each review thread - see
    [[#model-review-comment]]
3. Alice sends an [=Offer=] activity, in which:
    - [=target=] refers to *T*
    - [=object=] is a [=Review=] object in which:
        - [=context=] refers to *t*
        - [=attachment=] refers to the [=Note=] that is the summary comment
        - [=verdict=] is the review approval status
        - [=reviewIsBinding=] may be set to `true` if Alice has [=write=] or
            higher access to *T*
        - [=object=] is *t*'s [=Offer=] attachment's [=object=]'s URI, i.e. the
            latest version of changes proposed in *t*
        - [=reviewThreads=] specifies zero or more [=ReviewThread=] objects, in
            each of which:
            - [=target=] is a [=CodeQuote=] object, see [[#model-review-code]]
            - [=object=] refers to a [=Note=] that is the thread's top comment
    - [=capability=] refers to a [=Grant=] that authorizes the action (for
        [=report=] it MAY be omitted, but if [=reviewIsBinding=] is `true` it
        MUST be provided)
4. *T* publishes the [=Review=], assigning [=id=] URIs to the [=Review=] and to
    each of the [=ReviewThread=]s
5. *T* updates *t*'s [=individualApprovals=] collection, adding or updating the
    item for Alice
6. *T* sends an [=Accept=] in which:
    - [=object=] refers to the [=Offer=]
    - [=result=] is the [=id=] URI of the newly published [=Review=]

### Comment on a review ### {#s2s-mr-review-comment}

Review comments are [=Note=]s, optionally with [=Suggestion=] attachments, and
with [=context=] referring to a [=ReviewThread=]. See [[#model-review-comment]]
and [[#commenting]].

### Resolve a review thread ### {#s2s-mr-review-resolve}

Minimal required role: [=write=]

Given:

- A [=PatchTracker=], *T*
- A merge request (i.e. [=Ticket=]) under *T*, *t*
- A [=ReviewThread=] under one of the [=Review=]s of *t*, *h*
- A [=Person=] with [=write=] access to *T*, Alice

If *h* is open, i.e. its [=isResolved=] isn't specified or is set to False, *h*
may be closed via the following steps:

1. Alice sends *T* a [=Resolve=] activity where:
    - [=object=] is the URI of *h*
    - [=capability=] is the URI of a [=Grant=] authorizing the action
2. *T* sets *h*'s [=isResolved=] to True, and MAY set [=resolvedBy=] to Alice
    or to the [=Resolve=], and MAY set [=resolved=] to the current time
3. *T* publishes an [=Accept=] whose [=object=] points to the [=Resolve=]

### Unresolve a review thread ### {#s2s-mr-review-unresolve}

Minimal required role: [=write=]

Given:

- A [=PatchTracker=], *T*
- A merge request (i.e. [=Ticket=]) under *T*, *t*
- A [=ReviewThread=] under one of the [=Review=]s of *t*, *h*
- A [=Person=] with [=write=] access to *T*, Alice

If *h* is closed, i.e. its [=isResolved=] is True, *h* may be reopened via the
following steps:

1. Alice sends *T* a [=Undo=] activity whose:
    - [=object=] is a [=Resolve=] activity whose [=object=] is the URI of *h*
    - [=capability=] is the URI of a [=Grant=] authorizing the action
2. *T* sets *h*'s [=isResolved=] to False
3. *T* publishes an [=Accept=] whose [=object=] points to the [=Undo=]

### Dismiss a review ### {#s2s-mr-review-dismiss}

Minimal required role: [=write=]

Given:

- A [=PatchTracker=], *T*
- A merge request (i.e. [=Ticket=]) under *T*, *t*
- An [=Approval=] under *t*, *a*
- A [=Person=] with [=write=] access to *T*, Alice

Alice can switch the status of *a* to be [=dismissed=] (or
[=dismissedAwaiting=], in case this review is required by a rule or codeowner
setting) via the following steps:

1. Alice sends *T* a [=Undo=] activity whose:
    - [=object=] refers to *a*
    - [=capability=] is the URI of a [=Grant=] authorizing the action
2. *T* sets *a*'s [=approvalStatus=] to [=dismissed=] (or
    [=dismissedAwaiting=], see above) and updates the required approval count
    if needed
3. *T* publishes an [=Accept=] whose [=object=] points to the [=Undo=]

### Request a review ### {#s2s-mr-review-request}

Minimal required role: [=triage=]

Given:

- A [=PatchTracker=], *T*
- A merge request (i.e. [=Ticket=]) under *T*, *t*
- An [=Approval=] under *t*, *a*
- A [=Person=] with [=triage=] access to *T*, Alice
- A list of [=Person=]s and [=Team=]s requested to review

1. Alice sends *T* a [=Invite=] activity whose:
    - [=object=] refers to the people and teams
    - [=target=] refers to *t*'s [=individualApprovals=] and [=teamApprovals=]
    - [=capability=] is the URI of a [=Grant=] authorizing the action
2. *T* makes updates, for each person or team listed:
    - If there's no review by it, an [=Approval=] is added to the appropriate
        collection
    - If there's an existing approval, verify its current status is
        [=dismissed=], and now its status changes to [=dismissedAwaiting=], and
        [=reviewIsRequested=] changes to `true`
3. *T* publishes an [=Accept=] whose:
    - [=object=] points to the [=Invite=]
    - [=result=] refers to all the [=Approval=]s added or updated

### Indicate a pending review ### {#s2s-mr-review-pending}

Minimal required role: [=report=]

Given:

- A [=PatchTracker=], *T*
- A merge request (i.e. [=Ticket=]) under *T*, *t*
- A [=Person=] with [=report=] access to *T*, Alice

Alice can announce she has a review on *t* in progress, via the following steps:

1. Alice sends an [=Announce=] activity, in which:
    - [=target=] refers to *T*
    - [=object=] is a [=Review=] object in which:
        - [=context=] refers to *t*
    - [=capability=] refers to a [=Grant=] that authorizes the action (if *T*
        is a public resource it MAY be omitted)
2. *T* makes updates:
    - If there's no existing [=Approval=] by Alice, a new one is added to
        *t*'s [=individualApprovals=]
    - If there's an existing one, its [=newReviewInProgress=] is set to `true`
3. *T* sends an [=Accept=] in which:
    - [=object=] refers to the [=Announce=]
    - [=result=] refers to the [=Approval=] that has been added or updated

### List a review under a team ### {#s2s-mr-review-team-list}

Minimal required role: [=write=]

Given:

- A [=PatchTracker=], *M*
- A [=Team=], *T*
- A merge request (i.e. [=Ticket=]) under *M*, *m*
- A [=Person=] with [=write=] access to *T*, Alice
- An individual [=Approval=] under *m*, *a*
- A team [=Approval=] under *m*, *b*, attributed to *T*

Alice can mark *a* as a review made in the name of the team *T* via the
following steps:

1. Alice sends an [=Add=] activity, in which:
    - [=object=] refers to *a*
    - [=target=] refers to *b*
    - [=capability=] refers to a [=Grant=] that authorizes the action
2. *T* sends an [=Accept=] activity, whose [=object=] refers to the [=Add=]
3. *M* inserts *a* to *b*'s [=attachment=]s
4. *M* sends an [=Accept=] whose [=object=] refers to the [=Add=]

### Send a team approval ### {#s2s-mr-review-team-approve}

Minimal required role: [=write=]

Given:

- A [=PatchTracker=], *M*
- A [=Team=], *T*
- A merge request (i.e. [=Ticket=]) under *M*, *m*
- A [=Person=] with [=write=] access to *T*, Alice
- A team [=Approval=] under *m*, *a*, attributed to *T*

Alice can switch the status of *a* to [=approve=] via the following steps:

1. Alice sends a [=Resolve=] activity, in which:
    - [=object=] refers to *a*
    - [=capability=] refers to a [=Grant=] that authorizes the action
2. *T* sends an [=Offer=] activity, in which:
    - [=target=] refers to *M*
    - [=object=] is a [=Review=] object in which:
        - [=context=] refers to *m*
        - [=verdict=] is [=approve=]
        - [=object=] is *m*'s [=Offer=] attachment's [=object=]'s URI, i.e. the
            latest version of changes proposed in *m*
3. *M* assigns an [=id=] URI to the Review and updates *a*'s status
4. *M* sends an [=Accept=] whose:
    - [=object=] refers to the [=Offer=]
    - [=result=] refers to the new [=Review=]

## Issue and MR Dependencies ## {#s2s-dep}

Issues and Merge Requests may have various relationships to other issues/MRs.
This section talks about [=Ticket=] dependencies, and is relevant to both
issues and MRs. It's also possible for an issue to depend on a MR and vice
versa.

A dependency between [=Ticket=]s doesn't require consent of both sides. It
requires consent only from the "parent", i.e. the ticket that depends on
another ticket (the latter is therefore the "child").

### Add ### {#s2s-dep-add}

Minimal required role: [=triage=]

Given:

- A [=TicketTracker=] or [=PatchTracker=], *P*
- A [=TicketTracker=] or [=PatchTracker=], *C*
- *P* and *C* may be the same actor or be different actors
- A [=Ticket=] under *P*, *p*
- A [=Ticket=] under *C*, *c*
- An actor with [=triage=] access to *P*, Alice

Alice may create a dependency of *p* on *c* via the following steps:

1. Alice sends *P* and *C* an [=Offer=] activity where:
    - [=target=] refers to *P*
    - [=object=] is an object where:
        - [=type=] is [=TicketDependency=]
        - [=subject=] refers to *p*
        - [=relationship=] is [=dependsOn=]
        - [=object=] refers to *c*
    - [=capability=] refers to a [=Grant=] authorizing the operation
2. *P* validates the authorization and inserts the [=TicketDependency=] object
    to *p*'s [=dependencies=] collection, assigning it an [=id=]
3. *P* sends an [=Accept=] activity where:
    - [=object=] refers to the [=Offer=]
    - [=result=] refers to the new [=TicketDependency=]
4. *C*, seeing the [=Offer=] and the [=Accept=], inserts the
    [=TicketDependency=] to *c*'s [=dependants=] collection

### Remove ### {#s2s-dep-remove}

Minimal required role: [=triage=]

Given:

- A [=TicketTracker=] or [=PatchTracker=], *P*
- A [=TicketTracker=] or [=PatchTracker=], *C*
- *P* and *C* may be the same actor or be different actors
- A [=Ticket=] under *P*, *p*
- A [=Ticket=] under *C*, *c*
- An actor with [=triage=] access to *P*, Alice
- *p* depends on *c*

Alice may remove the dependency of *p* on *c* via the following steps:

1. Alice sends *P* and *C* a [=Delete=] activity where:
    - [=origin=] refers to *P*
    - [=object=] refers to the [=TicketDependency=]
    - [=capability=] refers to a [=Grant=] authorizing the operation
2. *P* validates the authorization and removes the [=TicketDependency=] object
    from *p*'s [=dependencies=] collection
3. *P* sends an [=Accept=] activity where [=object=] refers to the [=Delete=]
4. *C*, seeing the [=Delete=] and the [=Accept=], removes the
    [=TicketDependency=] from *c*'s [=dependants=] collection

## Commenting ## {#commenting}

Minimal required role: [=report=]

A comment on a ForgeFed resource object (such as tickets, merge requests) MUST
be published as a [=Create=] activity, in which [=object=] is a [=Note=] with
fields as described in [[#Comment]].

In the following example, Luke replies to Aviva's comment under a merge request
he submitted earlier against her Game Of Life simulation app repository:

<div class=example>
<xmp highlight=json-ld>
{
    "@context": "https://www.w3.org/ns/activitystreams",
    "id": "https://forge.example/luke/outbox/rLaYo",
    "type": "Create",
    "actor": "https://forge.example/luke",
    "to": [
        "https://forge.example/luke/followers",
        "https://dev.example/aviva/game-of-life",
        "https://dev.example/aviva/game-of-life/followers",
        "https://dev.example/aviva/game-of-life/team",
        "https://dev.example/aviva/game-of-life/merge-requests/19/followers",
        "https://dev.example/aviva/game-of-life/merge-requests/19/team"
    ],
    "object": {
        "id": "https://forge.example/luke/comments/rD05r",
        "type": "Note",
        "attributedTo": "https://forge.example/luke",
        "to": [
            "https://forge.example/luke/followers",
            "https://dev.example/aviva/game-of-life",
            "https://dev.example/aviva/game-of-life/followers",
            "https://dev.example/aviva/game-of-life/team",
            "https://dev.example/aviva/game-of-life/merge-requests/19/followers",
            "https://dev.example/aviva/game-of-life/merge-requests/19/team"
        ],
        "context": "https://dev.example/aviva/game-of-life/merge-requests/19",
        "inReplyTo": "https://dev.example/aviva/comments/E9AGE",
        "mediaType": "text/html",
        "content": "<p>Thank you for the review! I'll submit a correction ASAP</p>",
        "source": {
            "mediaType": "text/markdown; variant=Commonmark",
            "content": "Thank you for the review! I'll submit a correction ASAP"
        },
        "published": "2019-11-06T20:49:05.604488Z"
    }
}
</xmp>
</div>

## Granting access to shared resources ## {#managing-access}

Minimal required role: [=admin=]

An actor that wishes to give other specific actors access to view or modify it
(or a child object of it), SHOULD do so according to the following
instructions.

### Object capabilities

#### Introduction #### {#s2s-grant-simple}

An Object Capability (or in short OCap or OCAP) is a token providing access to
certain operations on a certain resource. An actor wishing to act on a resource
provides the token to the resource, alongside the Activity they wish to
perform. The resource verifies the token, and if and only if it finds the token
valid, and access to the requested Activity is allowed by the token, *then* the
resource allows the Activity to be performed.

The token provided by the actor to the resource, i.e. the OCAP, is the ID URI
of a previously published [=Grant=] activity.

The fundamental steps for accessing shared resources using OCAPs are:

1.  The actor managing the resource (which may be the resource itself) sends a
    `Grant` activity to the actor to whom it wishes to grant access
2.  When the actor who received the access wishes to operate on the resource,
    it sends the activity to the actor managing the resource, along with the ID
    URI of the `Grant` sent in step 1
3.  The actor managing the resource verifies the access provided by the `Grant`
    whose ID URI is provided, and allows the activity to be performed only if
    the verification passes

Providing the `Grant` ID URI like that when requesting to interact with a
resource is called an *invocation* of the `Grant`. There is another operation
possible with a `Grant` though: An actor can *delegate* a `Grant` it has
received, i.e. pass on the access, giving it to more actors. Delegation is
covered in a [later section](#s2s-grant-flow); for now let's assume `Grant`s
are used only for invocation. We therefore get the following simplified
validation process.

When an actor *R* receives from actor *A* a request to access/modify a resource
*r*, where the request is expressed as an activity *a* whose
[=capability=] field specifies some other activity *g*, then *R*
can validate *a* (i.e. decide whether or not to perform the requested action)
using the following steps:

1. Resource *r* MUST be a resource that *R* manages (it may be *R* itself)
2. *g*'s [=type=] MUST be [=Grant=]
3. *g*'s [=context=] MUST be *r*
4. *g*'s [=target=] MUST be *A*
5. Verify that *g*'s [=startTime=] <= now < *g*'s [=endTime=]
6. Verify that *g* doesn't specify [=delegates=]
7. *g*'s [=actor=] MUST be *R*
8. Verify that *R* indeed published *g* and considers it an active grant
    (i.e. *R* hasn't disabled/revoked it)
9. *checkLeaf(g):*
    1. *g*'s [=allows=] MUST be [=invoke=]
    2. Actor *A* SHOULD be of a [=type=] to which *R* allows to perform
        activity *a* on resource *r*, i.e. *A* should probably be a [=Person=],
        or some automated service/bot
10. Verify that the action being requested by activity *a* to perform on
    resource *r* is within what *R* permits for the [=Role=] specified by *g*'s
    [=object=]

At this point, activity *a* is considered authorized, and the requested action
may be performed.

#### Direct Granting #### {#direct-granting}

When an actor *R*, managing some resource *r*, wishes to allow some other actor
*A* to interact with *r*, under the conditions and permissions specified by
[=Role=] *p*, then actor *R* can send to actor *A* a [=Grant=] activity
with the following properties:

- [=actor=]: Specifies actor *R*
- [=context=]: Specifies resource *r*
- [=target=]: Specifies actor *A*
- [=object=]: Specifies role *p*
- [=startTime=]: (optional) The time at which the Grant becomes valid
- [=endTime=]: (recommended) The time at which the Grant expires
- [=allows=]: Specifies [=invoke=]
- [=delegates=]: Not used

#### Public and Collective Granting #### {#public-granting}

An object typically has certain operations that it allows any actor to perform,
and certain operations that only explicitly authorized actors have access to.
For example, on a typical public hosting platform for FOSS projects, opening an
issue or PR doesn't require special permissions. But pushing commits or merging
a PR does.

In a [=Grant=] activity this can be done using the [=target=] field. Instead of
pointing to a specific actor, it can alternatively have a special value
referring to a certain audience of actors. The supported special values are:

- [=Public=]: Refers to all actors on the network, i.e. this Grant can be used
    by any actor to manipulate the resource. We'll refer to such a Grant as a
    public-Grant.

Using the [=publicCapability=] property, an object can specify a public-Grant
to be available to anyone wishing to interact with the object.

#### Granting the delegate role #### {#grant-delegate}

A special case of direct granting is *granting permission to delegate*: If role
*p* is [=delegate=], then the `Grant` [=actor=] is allowing the
[=target=] to delegate `Grant`s to the [=actor=], i.e. to send `Grant`s meant
for delegation or `Grants` that are themselves delegations of other `Grant`s
(either start a chain, or extend a chain that some other actor started). More
on delegation in the next sections.

When an actor *A* wishes to allow some other actor *R* to delegate `Grant`s to
actor *A*, then actor *A* can send to actor *R* a [=Grant=] activity
with the following properties:

- [=actor=]: Specifies actor *A*
- [=context=]: Specifies actor *A*
- [=target=]: Specifies actor *R*
- [=object=]: Specifies [=delegate=]
- [=startTime=]: (optional) The time at which the Grant becomes valid
- [=endTime=]: (recommended) The time at which the Grant expires
- [=allows=]: Specifies [=invoke=]
- [=delegates=]: Not used

#### Starting a delegation chain #### {#start-grant-chain}

When an actor *R*, managing some resource *r*, wishes to allow or request some
other actor *A* to delegate some access-to-*r*-under-role-*p* to certain (or
any) other actors that *A* knows, then actor *R* can send to actor *A* a
[=Grant=] activity with the following properties:

- [=actor=]: Specifies actor *R*
- [=context=]: Specifies resource *r*
- [=target=]: Specifies actor *A*
- [=object=]: Specifies role *p*
- [=startTime=]: (optional) The time at which the Grant becomes valid
- [=endTime=]: (recommended) The time at which the Grant expires
- [=allows=]: Specifies the conditions under which actor *A* may
    delegate this `Grant` (i.e. conditions under which actor *R* will consider
    the delegation valid when verifying the chain), and what the recipients of
    the delegtaions that *A* will send (which are themselves `Grant` activites)
    are allowed to do with these `Grants` (invoke? further delegate to certain
    other actors?)
- [=delegates=]: Not used
- [=capability=]: *(optional)* Specifies a
    [[#grant-delegate|delegate Grant]] previously given by *A* to *R*

The following cases are supported in ForgeFed for starting a delegation chain.
The term 'component' used below refers to a forge related service actor. This
may be a service of a [=type=] defined in ForgeFed (such as
[=Repository=], [=TicketTracker=],
[=PatchTracker=]), or a service defined in some extension.

1. [=actor=] is a component, [=target=] is a [=Project=]
    - Scenario: A component delegates access-to-a-resource-it-manages (which is
        often simply itself) to a project to which the component belongs
    - [=allows=] value to use: [=gatherAndConvey=]
    - Conditions for the target project:
        * It SHOULD delegate the `Grant`, allowing only `gatherAndConvey`, to
            its own parent projects
        * It SHOULD delegate the `Grant`, allowing only `distribute`, to teams
            to which it allows to access it
        * It SHOULD delegate the `Grant`, allowing only `invoke`, to people and
            bots to which it allows to access it
        * It SHOULD NOT make any other delegation of this `Grant`, and SHOULD
            NOT invoke it
2. [=actor=] is a [=Project=], [=target=] is a parent [=Project=] of it
    - Scenario: A project delegates access-to-a-resource-itself to its parent
        project
    - [=allows=] value to use: Same as 1
    - Conditions for the target project: Same as 1
3. [=actor=] is a component, [=target=] is a [=Team=]
    - Scenario: A component delegates access-to-a-resource-it-manages to a team
        that has been approved to access the component
    - [=allows=] value to use: [=distribute=]
    - Conditions for the target team:
        * It SHOULD delegate the `Grant`, allowing `distribute` only, to its
            subteams
        * It SHOULD delegate the `Grant`, allowing `invoke` only, to its
            members
        * It SHOULD NOT make any other delegation of this `Grant`, and SHOULD
            NOT invoke it
4. [=actor=] is a [=Project=], [=target=] is a [=Team=]
    - Scenario: A project delegates access-to-itself to a team that has been
        approved to access the project
    - [=allows=] value to use: Same as 3
    - Conditions for the target team: Same as 3
5. [=actor=] is a [=Team=], [=target=] is a [=Team=]
    - Scenario: A team delegates access-to-itself to its newly added oversight
        team
    - [=allows=] value to use: Same as 3
    - Conditions for the target team: Same as 3

#### Extending a delegation chain #### {#extending-a-delegation-chain}

When an actor *A* receives a [=Grant=] activity *g* where the
[=target=] is *A*, and wishes to pass on the granted access to some other actor
*B* (who isn't the [=actor=] of that `Grant`), then actor *A* can do so by
sending to actor *B* a new `Grant` activity *h* in which:

- [=actor=] is actor *A*
- [=context=] (i.e. the resource) is same as *g*'s [=context=]
- [=target=] is actor *B*
- [=object=] (i.e. the granted role) is either *g*'s [=object=] or a
    lower-access role than *g*'s [=object=], i.e. provides a subset of the
    permissions that *g*'s [=object=] provides (the latter case is called
    *attenuation*)
- [=startTime=]: *(optional)* The time at which this Grant becomes valid
- [=endTime=]: *(recommended)* The time at which this Grant expires
- [=allows=]: Specifies the conditions under which actor *B* may
    delegate this `Grant` (i.e. conditions under which the delegation will be
    considered valid when verifying the chain), and what the recipients of
    the delegtaions that *B* will send (which are themselves `Grant` activites)
    are allowed to do with these `Grants` (invoke? further delegate to certain
    other actors?)
- [=delegates=] is activity *g*
- [=capability=]: *(optional)* Specifies a
    [delegate Grant](#grant-delegate) previously given by *B* to *A*
- [=result=]: a URI that will be used later to verify that *h* is still active
    and hasn't been revoked. Alternatively, an object with [=id=] and
    [=duration=] as described below.

The [=result=] URI MUST be provided whenever extending a delegation chain. It
MUST be a URI that actor *A* controls, i.e. decides what will be returned by
HTTP requests to that URI. Requirements:

- From the moment that actor *A* publishes activity *h*, as long as actor *A*
    considers *h* an active `Grant` and hasn't revoked it, any HTTP HEAD or HTTP
    GET request the [=result=] URI MUST return an HTTP response status 204 or 200.
- If later activity *h* is revoked, or actor *A* is deleted, then from the
    moment that actor *A* considers *h* deactivated, any HTTP HEAD or HTTP GET
    request to the [=result=] URI MUST NOT return an HTTP response status in the
    200-299 range. The response status SHOULD be 410 or 404.

[=result=] MAY instead specify a JSON object in which:

- [=id=] is the URI as described above
- *(optional)* [=duration=] specifies a duration that allows the recovation URI
    check to be skipped, if the duration hasn't yet passed since the last check
    of the URI. If [=duration=] is specified, it MUST be positive and include
    only an integral number of seconds that is less than `2^63`, and no other
    component. In other words, its format is: The string "PT", then the
    integer, then the string "S".

In the following cases, *g* is a *request* for actor *A* to extend the
delegation chain, and actor *A* SHOULD extend the chain by sending `Grant`
activities, as described for each case.

The term 'component' used below refers to a forge related service actor. This
may be a service of a [=type=] defined in ForgeFed (such as
[=Repository=], [=TicketTracker=],
[=PatchTracker=]), or a service defined in some extension.

1. Actor *A* is a [=Project=], AND *g*'s [=actor=] is either a
    [=component=] of *A* or a [=subproject=] of
    *A*, AND *g*'s [=allows=] is a single value
    [=gatherAndConvey=]
    - Scenario: Project *A* received some access from a component/subproject of
        it, and is requested to pass it on its member people, to its member
        teams, and to its parent projects
    - Requirements for extending the delegation chain:
        1. For each parent project *P* of project *A*, project *A* SHOULD
            publish and deliver to *P* a `Grant` activity in which:
            - [=actor=] is project *A*
            - [=context=] (i.e. the resource) is same as *g*'s [=context=]
            - [=target=] is project *P*
            - [=object=] (i.e. the granted role) is either *g*'s [=object=] or
                a lower-access role than *g*'s [=object=]
            - [=allows=] is a single value [=gatherAndConvey=]
            - [=delegates=] is activity *g*
            - [=capability=]: *(optional)* Specifies a
                [delegate Grant](#grant-delegate) previously given by *P* to *A*
            - [=result=]: a URI that will be used later to verify that *h* is
                still active and hasn't been revoked, or a JSON object as
                describes above

        2. For each team *T* that project *A* considers a member team with role
            *p*, project *A* SHOULD publish and deliver to *T* a `Grant`
            activity in which:
            - [=actor=] is project *A*
            - [=context=] (i.e. the resource) is same as *g*'s [=context=]
            - [=target=] is team *T*
            - [=object=] (i.e. the granted role) is the lower-access role
                among *g*'s [=object=] and *p*
            - [=allows=] is a single value [=distribute=]
            - [=delegates=] is activity *g*
            - [=capability=]: *(optional)* Specifies a
                [delegate Grant](#grant-delegate) previously given by *T* to *A*
            - [=result=]: a URI that will be used later to verify that *h* is
                still active and hasn't been revoked, or a JSON object as
                describes above

        3. For each [=Person=] or automated service bot *M* (that isn't a team)
            that project *A* considers a member with role *p*, project *A*
            SHOULD publish and deliver to *M* a `Grant` activity in which:
            - [=actor=] is project *A*
            - [=context=] (i.e. the resource) is same as *g*'s [=context=]
            - [=target=] is actor *M*
            - [=object=] (i.e. the granted role) is the lower-access role
                among *g*'s [=object=] and *p*
            - [=allows=] is a single value [=invoke=]
            - [=delegates=] is activity *g*
            - [=capability=]: *(optional)* Specifies a
                [delegate Grant](#grant-delegate) previously given by *M* to *A*
            - [=result=]: a URI that will be used later to verify that *h* is
                still active and hasn't been revoked, or a JSON object as
                describes above

        4. Project *A* MUST NOT make any other delegations of *g*, and SHOULD
            NOT try to invoke it

2. Actor *A* is a [=Team=], AND *g*'s [=actor=] is either a
    component/[=Project=] in which *A* is a member or a
    parent team (see [=subteams=]) of *A* or a team overseeing *A* (see
    [=oversees=]), AND *g*'s [=allows=] is a single value [=distribute=]
    - Scenario: Team *A* received some access from a component/project that
        considers *A* a member team, or from a parent/overseen team of *A*, and
        *A* is requested to pass it on its member people and to its subteams
    - Requirements for extending the delegation chain:
        1. For each team *T* that team *A* considers a
            [=subteam=], team *A* SHOULD publish and deliver to *T*
            a `Grant` activity in which:
            - [=actor=] is team *A*
            - [=context=] (i.e. the resource) is same as *g*'s [=context=]
            - [=target=] is team *T*
            - [=object=] (i.e. the granted role) is the same as
                *g*'s [=object=]
            - [=allows=] is a single value [=distribute=]
            - [=delegates=] is activity *g*
            - [=capability=]: *(optional)* Specifies a
                [delegate Grant](#grant-delegate) previously given by *T* to *A*

        2. For each [=Person=] or automated service bot *M* (that isn't a team)
            that team *A* considers a member with role *p*, team *A*
            SHOULD publish and deliver to *M* a `Grant` activity in which:
            - [=actor=] is team *A*
            - [=context=] (i.e. the resource) is same as *g*'s [=context=]
            - [=target=] is actor *M*
            - [=object=] (i.e. the granted role) is the lower-access role
                among:
                    1. *g*'s [=object=]
                    2. *p*
                    3. If team *A* has a [=roleFilter=]: The role specified
                        under the actor type that matches the [=type=] of *g*'s
                        [=context=]
            - [=allows=] is a single value [=invoke=]
            - [=delegates=] is activity *g*
            - [=capability=]: *(optional)* Specifies a
                [delegate Grant](#grant-delegate) previously given by *M* to *A*

        3. Team *A* MUST NOT make any other delegations of *g*, and SHOULD NOT
            try to invoke it

#### Revoking a Grant #### {#s2s-revoke}

At any point after an actor *A* publishes a [[#Grant]] in which it
grants some actor *B* access to a resource that actor *A* manages, actor *A*
MAY cancel that `Grant`, deciding it's no longer a valid OCAP to use via the
[=capability=] property of activies that actor *B* sends.

If actor *A* cancels such a `Grant`, it SHOULD publish and deliver, at least to
actor *B*, a [=Revoke=] activity notifying about the canceled
`Grant`. In the `Revoke` activity, actor *A* MUST specify the Grants being
revoked, via the [=object=] property, where each Grant is specified in one of
the following ways:

1. The Grant is specified by its `id` URI
2. The whole Grant activity object is provided, and MUST contain an
    [[fep-8b32|integrity proof]]

Additional requirements:

- Implementations displaying a `Revoke` activity or an interpretation of it in
    a human interface MUST examine the `Revoke`'s [=object=] property if it is
    present, check if any of the `Grant`s listed are delegations, and communicate
    that detail in the human interface

Once actor *A* publishes the `Revoke`, it MUST from now on refuse to execute
requests from actor *B* to access resources that actor *A* manages, coming as
activities that specify any of the canceled `Grant`s in the `capability`
property. If actor *A* receives such an activity from actor *B*, it SHOULD
publish and send back a [=Reject=] activity, whose [=object=] specifies the
activity that actor *B* sent.

If the `Grant` that actor *A* is revoking specifies a [=result=], then from now
on any HTTP HEAD request to the URI specified by [=result=] MUST NOT return an
HTTP response status in the 200-299 range. The returned status SHOULD be 410
or 404. See [Extending a delegation chain](#extending-a-delegation-chain) for
more information.

#### Verifying an invocation #### {#s2s-grant-flow}

A [previous section](#s2s-grant-simple) described *direct* usage of
[=Grant=]s, where the *resource actor* gives some access to a *target
actor*, and the *target actor* then uses it to interact with the resource.
Another way to give authorization is via delegation chains:

- The *resource actor* passes access to a *target actor*, allowing (or
    requesting) the *target actor* to pass this access (or reduced access) on to
    more actors
- If authorized by the delegation, those actors may further pass on the access
    (possibly reduced)
- Eventually, an actor that received such a delegation may use it to access the
    resource

Access is delegated using [=Grant=] activities as well, using the
[=delegates=] property to point from each `Grant` in the chain to
the previous one. The "direct" `Grant` discussed earlier is simply a delegation
chain of length 1.

When an actor *R* receives from actor *A* a request to access/modify a resource
*r*, where the request is expressed as an activity *a* whose
[=capability=] field specifies some other activity *g*, then *R*
can validate *a* (i.e. decide whether or not to perform the requested action)
using the following steps.

*R* begins by verifying that resource *r* is indeed a resource that *R* manages
(it may be *R* itself). Otherwise, verification has failed.

*R* proceeds by collecting the delegation chain in a list, by traversing the
chain backwards from the leaf all the way to the beginning of the chain. The
traversal starts with the list *L* being empty, and *R* examines activity *g*:

1. *g*'s [=type=] MUST be [=Grant=]
2. *g*'s [=context=] MUST be *r*
3. *g*'s [=target=] MUST be *A*
4. *g* MUST NOT already be listed in *L*
5. Verify that *g*'s [=startTime=] <= now < *g*'s [=endTime=]
6. Look at *g*'s [=delegates=]:
    - If *g* doesn't specify [=delegates=]:
        1. *g*'s [=actor=] MUST be *R*
        2. Verify that *R* indeed published *g* and considers it an active
            grant (i.e. *R* hasn't disabled/revoked it)
        3. Prepend *g* to the beginning of *L*, resulting with new list *M*
        4. We're done with the traversal step, the output is *M*
    - If *g*'s [=delegates=] is some activity *h*:
        1. *g*'s [=actor=] MUST NOT be *R*
        2. *g* MUST specify exactly one [=result=] URI
        3. Verify the [=result=]:
            - If it's an object with [=duration=] specified, and this duration
                of time hasn't yet passed since the last check, proceed without
                checking the URI
            - Otherwise, send an HTTP HEAD request to the URI, The HTTP
                response status MUST be 200 or 204
        4. Prepend *g* to the beginning of *L*, resulting with new list *M*
        5. Continue traversal by going back to step 1, but with *M* being the
            list, and with *g*'s [=actor=] instead of *A*, and now examining
            activity *h*

Issue: "Going back to step 1" refers to the top-level list item; should
probably tweak the CSS to display nested lists differently.

*R* proceeds by traversing the resulting list *L* from the beginning forward,
all the way to the leaf, validating and tracking attenuation in each step. *R*
starts this by examining the first item in *L*, let's call this item *g*:

1. Let *p* be *g*'s [=object=]
2. Examine *g*'s position in *L*:
    - If *g* is the last item in *L*:
        1. Perform *checkLeaf* on *g* (see below)
        2. Verify that the action being requested by activity *a* to perform on
            resource *r* is within what *R* permits for [=Role=] *p*.
        3. We're done with the traversal!
    - Otherwise:
        1. Let *h* be the next item after *g* in *L*
        2. Let *q* be *h*'s [=object=]
        3. The permissions that role *q* allows on resource *r* MUST be
            identical to or a subset of the permissios that role *p* allows on
            *r*
        4. Perform *checkItem* on *(g, h)* (see below)
        5. Continue traversal by going back to step 2, but with *h* instead of
            *g* and *q* instead of *p*

Issue: "Step 2" refers to the top-level one, need to tweak CSS for lists

The steps *checkLeaf* and *checkItem* mentioned above MAY be extended by
implementations, by using custom values in the [=allows=] property.
But here are the standard definitions, using the values defined in ForgeFed:

*checkLeaf (g):*

1. *g*'s [=allows=] MUST be [=invoke=]
2. *g*'s [=target=] (which is actor *A*, the sender of activity *a*) SHOULD be
    an actor of a [=type=] to which *R* allows to perform activity *a* on
    resource *r*, i.e. *A* should probably be a [=Person=], or some automated
    service/bot

*checkItem (g, h):*

1. *g* MUST specify exactly one value for [=allows=]
2. That value MUST be either [=gatherAndConvey=] or [=distribute=]
    - If it's [=gatherAndConvey=]:
        1. *g*'s [=target=] MUST be a [=Project=]
    - If it's [=distribute=]:
        1. *g*'s [=target=] MUST be a [=Team=]
        2. *h*'s [=allows=] MUST be either [=distribute=]
            or [=invoke=]

At this point, activity *a* is considered authorized, and the requested action
may be performed.

#### Identifying resources and their managing actors #### {#manager}

Some shared resources are themselves actors. Some shared resources aren't
actors, but they are child objects of actors. When some actor *A* wishes to
access a resource *R* and perform a certain operation, it needs to determine
which actor to contact in order to request that operation. Actor *A* then looks
at resource *R*, and the following MUST hold:

- Either the resource *R* isn't an actor (i.e. doesn't have an [=inbox=]) but
    does specify which actor manages it via the [=managedBy=] property;
- Or the resource *R* is an actor, i.e. it has an [=inbox=] (it doesn't have to
    specify [=managedBy=], but if it does, then it MUST refer to itself)

Therefore any object that wishes to be specified as the [=context=] of a
[=Grant=] MUST either be an actor or be [=managedBy=] an
actor.

#### Invoking a Grant

Invoking a [=Grant=] means using the `Grant` to authorize a request to
access or modify some resource. If some actor *A* wishes to access or modify a
resource *r*, using a `Grant` activity *g* for authorization, preconditions
for a successful invocation include:

- *g*'s [=target=] is actor *A* (or an audience that includes *A*, see
    [[#public-granting]])
- *g*'s [=context=] is either the resource *r*, or a resource in which *r* is
    contained, or the actor that [=managedBy|manages=] *r*
- *g*'s [=object=] is a [=Role=] that permits the kind of operation
    that actor *A* is requesting to do on resource *r*
- *g*'s [=allows=] is [=invoke=]
- *g*'s [=startTime=] <= now < *g*'s [=endTime=]

When actor *A* sends the activity *a* that requests to access or modify
resource *r*, it can use *g* for authorization by specifying its [=id=] URI in
the [=capability=] property of activity *a*.

To have a chance to access resource *r*, actor *A* needs to deliver activity
*a* to the actor that manages *r*. [See above](#manager) instructions for
determining who that actor is.

#### Time Bounds

A [=Grant=] activity MUST be considered valid for invocation (or as a valid
link in a delegation chain) if and only if the current time, at the time of
invocation, is within the time bounds defined by the [=Grant=]:

1. A [=Grant=] MAY specify a [=startTime=]: The time at which the Grant becomes
    valid. If specified, the [=Grant=] is valid only if the time of invocation
    is equal or greater than the [=startTime=]
2. A [=Grant=] SHOULD specify an [=endTime=]: The time at which the Grant
    expires. If specified, the [=Grant=] is valid only if the time of
    invocation is less than the [=endTime=]

Suggested default for picking the [=endTime=]: 6 months after publishing the
[=Grant=].

### Granting access ### {#granting-access}

#### Initial Grant upon resource creation

When an actor *A* requests to create a new shared resource *R*, and the
*resource actor* approves and creates it, then the *resource actor* SHOULD send
a `Grant` to actor *A*, which provides actor *A* with access to resource *R*.

The [=Role=] specified by the [=Grant=]'s [=object=] MUST be [=admin=], which
means full access to *R*, including the ability to gives access-to-*R* to more
actors (using an [=Invite=] activity, see below).

If such a `Grant` is sent by the *resource actor* upon the creation of resource
*R*, then the `Grant`'s [=fulfills=] property MUST be provided and
specify the ID URI of the activity (published by actor *A*) that requested to
create resource *R* (typically this would be a [=Create=] activity, see
[Object Publishing and Hosting](#publishing)).

If *R* is a [=Project=] or a [=Team=], additional steps occur:

1. *A*, seeing *R*'s Grant, publishes a [[#grant-delegate|delegate-Grant]]
    in which the [=target=] is *R* and [=capability=] is *R*'s Grant
2. From now on, whenever *R* wishes to
    [[#extending-a-delegation-chain|extend a Grant chain]] to *A*, it uses
    *A*'s delegate-Grant as the [=capability=]

#### Offering access using Invite activities #### {#access-via-invite}

When an actor *A* wishes to offer actor *B* access to resource *R* (where the
*resource actor* who manages *R* is neither *A* nor *B*), then actor *A* SHOULD
use an [=Invite=] activity, and the following steps:

1. Actor *A* publishes and delivers an [=Invite=], at least to actor
    *B* and to the *resource actor* of *R*, with a relevant
    [=capability=] (see [[#Invite]] for details on the properties to use)
2. If actor *B* wishes to have the offered access, it publishes and delivers
    (at least to the *resource actor* of *R*) an [=Accept=] activity whose
    [=object=] specifies the `Invite` sent by actor *A*
3. The *resource actor* of *R* receives the `Invite` and the `Accept` and:
    1. Verifies the `Invite` is authorized, as described above in
        [Verifying an invocation](#s2s-grant-flow)
    2. Verifies that the `Accept`'s [=object=] specifies the `Invite` and the
        `Accept`'s [=actor=] is the `Invite`'s [=object=]
    3. Publishes and delivers a [=Grant=] activity (see
        [[#Grant]] for more details on the properties) where:
        - [=object=] is the `Invite`'s [=instrument=]
        - [=context=] is the `Invite`'s [=target=], which is resource *R*
        - [=target=] is the `Invite`'s [=object=], which is actor *B*
        - [=fulfills=] is the `Invite`
        - [=allows=] is [=invoke=]
        - [=delegates=] isn't specified
4. *B* is now considered a collaborator in *R*!
5. If *R* is a [=Project=] or a [=Team=], additional steps occur:
    1. *B*, seeing *R*'s Grant, publishes a [[#grant-delegate|delegate-Grant]]
        in which the [=target=] is *R* and [=capability=] is *R*'s Grant
    2. From now on, whenever *R* wishes to
        [[#extending-a-delegation-chain|extend a Grant chain]] to *B*, it uses
        *B*'s delegate-Grant as the [=capability=]

Actor *B* can now use the URI of that new `Grant` as the
[=capability=] when it sends activities that access or
manipulate resource *R*.

#### Requesting access using Join activities #### {#access-via-join}

When an actor *A* wishes to request access to resource *R* (where the *resource
actor* who manages *R* isn't *A*), then actor *A* SHOULD use a
[=Join=] activity, and the following steps. There are two options detailed
below, depending on whether actor *A* has been previously given a
[=Grant=] authorizing it to gain access to resource *R* without
needing someone else to approve. For example, perhaps actor *A* already has
some access to a resource collection to which *R* belongs, and that access
allows *A* to freely `Join` *R* without needing to wait for human approval.

**Option 1: Actor *A* already has a `Grant` allowing it to gain access to *R*
without external approval:**

1. Actor *A* publishes and delivers a [=Join=], at least to the
    *resource actor* of *R*, with the relevant [=capability=] it
    has (see [[#Join]] for details on the properties
    to use)
2. The *resource actor* of *R* receives the `Join` and:
    1. Verifies the `Join` is authorized, as described above in
        [Verifying an invocation](#s2s-grant-flow)
    2. Publishes and delivers a [=Grant=] activity (see
        [[#Grant]] for more details on the
        properties) where:
        - [=object=] is the `Join`'s [=instrument=]
        - [=context=] is the `Join`'s [=object=], which is resource *R*
        - [=target=] is the `Join`'s [=actor=], which is actor *A*
        - [=fulfills=] is the `Join`
3. *A* is now considered a collaborator in *R*!
4. If *R* is a [=Project=] or a [=Team=], additional steps occur:
    1. *A*, seeing *R*'s Grant, publishes a [[#grant-delegate|delegate-Grant]]
        in which the [=target=] is *R* and [=capability=] is *R*'s Grant
    2. From now on, whenever *R* wishes to
        [[#extending-a-delegation-chain|extend a Grant chain]] to *A*, it uses
        *A*'s delegate-Grant as the [=capability=]

Actor *A* can now use the URI of that new `Grant` as the
[=capability=] when it sends activities that access or
manipulate resource *R*.

**Option 2: Actor *A* doesn't have (or chooses not to use) a `Grant` allowing
it to gain access to *R* without external approval:**

1. Actor *A* publishes and delivers a [=Join=], at least to the
    *resource actor* of *R* (see [[#Join]] for
    details on the properties to use)
2. If some actor *B*, that has previously received a `Grant` from the *resource
    actor* of *R* authorizing it to approve joins, sees the `Join` sent by actor
    *A* and decides to approve it, then actor *B* publishes and delivers (at
    least to the *resource actor* of *R*) an [=Accept=] activity where:
      - [=object=] specifies the `Join` sent by actor *A*
      - [=capability=] is the `Grant` mentioned above,
        authorizing to approve or deny Joins
3. The *resource actor* of *R* receives the `Join` and the `Accept` and:
    1. Verifies the `Accept` is authorized, as described above in
        [Verifying an invocation](#s2s-grant-flow)
    2. Verifies that the `Accept`'s [=object=] specifies the `Join`
    3. Publishes and delivers a [=Grant=] activity (see
        [[#Grant]] for more details on the properties) where:
        - [=object=] is the `Join`'s [=instrument=]
        - [=context=] is the `Join`'s [=object=], which is resource *R*
        - [=target=] is the `Join`'s [=actor=], which is actor *A*
        - [=fulfills=] is the `Join`
4. *A* is now considered a collaborator in *R*!
5. If *R* is a [=Project=] or a [=Team=], additional steps occur:
    1. *A*, seeing *R*'s Grant, publishes a [[#grant-delegate|delegate-Grant]]
        in which the [=target=] is *R* and [=capability=] is *R*'s Grant
    2. From now on, whenever *R* wishes to
        [[#extending-a-delegation-chain|extend a Grant chain]] to *A*, it uses
        *A*'s delegate-Grant as the [=capability=]

Actor *A* can now use the URI of that new `Grant` as the
[=capability=] when it sends activities that access or
manipulate resource *R*.

In step 2, actor *B* may choose to deny the request of actor *A*, by sending a
[=Reject=] activity (at least to the *resource actor* of *R*) where:

- [=object=] specifies the `Join` that actor *A* sent
- [=capability=] is the `Grant` mentioned in step 2, authorizing
    actor *B* to approve or deny Joins

If the *resource actor* of *R* receives the `Reject`:

1. It MUST verify the `Reject` is authorized, as described above in
    [Verifying an invocation](#s2s-grant-flow)
2. it MUST verify that the `Reject`'s [=object=] specifies the `Join`
2. Consider this `Join` request canceled: If actor *B*, or some other actor
    *C*, tries again to `Accept` the `Join`, then:
      1. The *resource actor* MUST NOT send a `Grant` to actor *A*, even if the
            `Accept` is authorized
      2. The *resource actor* MAY publish and deliver a `Reject` activity, at
            least to the actor that sent the `Accept`, where [=object=] specifies
            the `Accept`
4. It SHOULD publish and deliver a `Reject` activity, at least to actor *A*,
    where [=object=] specifies the `Join` that actor *A* sent

So, once a `Join` is rejected (using an authorized `Reject`), it cannot be
accepted. But actor *A* MAY send a new `Join`, which could then possibly get
accepted.

### Revoking access ### {#revoking-access}

#### Taking away access using Remove activities

When an actor *A* wishes to cancel the membership of another actor *B* (who
isn't *A*) in a shared resource *R*, invalidating any active
[[#Grant]]s that the *resource actor* of *R* has granted to actor
*B*, then actor *A* SHOULD use a [=Remove=] activity, and the following steps:

1. Actor *A* publishes and delivers a [=Remove=], at least to actor
    *B* and to the *resource actor* of *R*, with a relevant
    [=capability=] (see [[#Remove]]
    for details on the properties to use)
2. The *resource actor* of *R* receives the `Remove` and:
    1. Verifies the `Remove` is authorized, as described above in
        [Verifying an invocation](#s2s-grant-flow)
    2. Verifies that actor *B* indeed has active `Grant`s for accessing
        resource *R*
    3. Marks those Grants as disabled in its internal state
    4. Publishes and delivers a [[#Revoke]] activity, as described
        above in [Revoking a Grant](#s2s-revoke), where
        [=fulfills=] specifies the `Remove`

Actor *B* SHOULD no longer use the URI of any `Grant` that has been disabled as
the [=capability=] when it sends activities that access or
manipulate resource *R*.

#### Waiving access using Leave activities

When an actor *A* wishes to cancel their membership in a shared resource *R*
(where the *resource actor* who manages *R* isn't *A*), invalidating any active
[[#Grant]]s that the *resource actor* of *R* has granted to actor
*A*, then actor *A* SHOULD use a [=Leave=] activity, and the following steps:

1. Actor *A* publishes and delivers a [=Leave=], at least to the
    *resource actor* of *R* (see [[#Leave]] for
    details on the properties to use)
2. The *resource actor* of *R* receives the `Leave` and:
    1. Verifies that actor *A* indeed has active `Grant`s for accessing
        resource *R*
    2. Marks those Grants as disabled in its internal state
    3. Publishes and delivers a [[#Revoke]] activity, as described
        above in [Revoking a Grant](#s2s-revoke), where
        [=fulfills=] specifies the `Leave`

Actor *A* SHOULD no longer use the URI of any `Grant` that has been disabled as
the [=capability=] when it sends activities that access or
manipulate resource *R*.

#### Requesting to disable specific Grants using Undo

When an actor *A* wishes to deactivate a specific [[#Grant]] activity
(or multiple `Grant`s), providing access to view or manipulate some resource
*R* (where the *resource actor* of *R* isn't *A*), then actor *A* SHOULD use an
[=Undo=] activity, and the following steps. The actor *B* to whom
access-to-resource-*R* was given by the `Grant` may be actor *A* itself, or
some other actor, as long as actor *A* is authorized by the *resource actor* of
*R* to deactivate that `Grant`.

NOTE: Upon a successful `Undo`, if actor *B* doesn't have any active `Grants`
left, that allow access to resource *R*, then the *resource actor* of *R* MAY
remove actor *B*'s membership in *R*, or it MAY consider actor *B* a member
without access.

1. Actor *A* publishes and delivers an [=Undo=], at least to the
    *resource actor* of *R* (see [[#undo-grant]] for
    details on the properties to use)
2. The *resource actor* of *R* receives the `Undo` and:
    1. Verifies the `Undo` is authorized, as described above in
        [Verifying an invocation](#s2s-grant-flow)
    2. Verifies that actor *B* indeed has all the active `Grant`s for accessing
        resource *R*, that are listed as [=object=]s of the `Undo` (if more than
        one `Grant` is listed, the [=target=] of all the `Grant`s MUST be
        identical)
    3. Marks all of those Grants as disabled in its internal state
    4. Publishes and delivers a [[#Revoke]] activity, at least to
        actors *A* and *B*, as described above in
        [Revoking a Grant](#s2s-revoke), where:
          - [=object=] MUST specify all the deactivated `Grant`s
          - [=fulfills=] MUST specify the `Undo`

Actor *B* SHOULD no longer use the URI of any `Grant` that has been disabled as
the [=capability=] when it sends activities that access or
manipulate resource *R*.

### Example

Aviva creates a new [=Repository=] for her 3D Tree Growth
Simulation software:

<div class=example>
<xmp highlight=json-ld>
{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://forgefed.org/ns"
    ],
    "id": "https://forge.community/users/aviva/outbox/oU6QGAqr-create-treesim",
    "type": "Create",
    "actor": "https://forge.community/users/aviva",
    "to": [
        "https://forge.community/users/aviva/followers"
    ],
    "object": {
        "id": "https://forge.community/repos/treesim",
        "type": "Repository",
        "name": "Tree Growth 3D Simulation",
        "summary": "A graphical simulation of trees growing"
    }
}
</xmp>
</div>

The newly created *treesim* `Repository` automatically sends back a `Grant` to
Aviva, allowing her full access to the repo:

<div class=example>
<xmp highlight=json-ld>
{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://forgefed.org/ns"
    ],
    "id": "https://forge.community/repos/treesim/outbox/2NwyPWMX-grant-admin-to-aviva",
    "type": "Grant",
    "actor": "https://forge.community/repos/treesim",
    "to": [
        "https://forge.community/aviva",
        "https://forge.community/aviva/followers"
    ],
    "object": "admin",
    "context": "https://forge.community/repos/treesim",
    "target": "https://forge.community/aviva",
    "fulfills": "https://forge.community/users/aviva/outbox/oU6QGAqr-create-treesim",
    "allows": "invoke",
    "endTime": "2023-12-31T23:00:00-08:00"
}
</xmp>
</div>

Aviva can now use this `Grant`, e.g. to update the repo's description text:

<div class=example>
<xmp highlight=json-ld>
{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://forgefed.org/ns"
    ],
    "id": "https://forge.community/users/aviva/outbox/RmTygyuj",
    "type": "Update",
    "actor": "https://forge.community/users/aviva",
    "to": [
        "https://forge.community/users/aviva/followers",
        "https://forge.community/repos/treesim",
        "https://forge.community/repos/treesim/followers"
    ],
    "object": {
        "id": "https://forge.community/repos/treesim",
        "type": "Repository",
        "name": "Tree Growth 3D Simulation",
        "summary": "Tree growth 3D simulator for my nature exploration game"
    },
    "capability": "https://forge.community/repos/treesim/outbox/2NwyPWMX-grant-admin-to-aviva"
}
</xmp>
</div>

Aviva wants to keep track of events related to the *treesim* repo:

<div class=example>
<xmp highlight=json-ld>
{
    "@context": "https://www.w3.org/ns/activitystreams",
    "id": "https://forge.community/users/aviva/outbox/gqtpAhm2",
    "type": "Follow",
    "actor": "https://forge.community/users/aviva",
    "to": "https://forge.community/repos/treesim",
    "object": "https://forge.community/repos/treesim",
}
</xmp>
</div>

Aviva can invite Luke to have access to the *treesim* repo:

<div class=example>
<xmp highlight=json-ld>
{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://forgefed.org/ns"
    ],
    "id": "https://forge.community/users/aviva/outbox/qfrEGqnC-invite-luke",
    "type": "Invite",
    "actor": "https://forge.community/users/aviva",
    "to": [
        "https://forge.community/aviva/followers",
        "https://forge.community/repos/treesim",
        "https://forge.community/repos/treesim/followers",
        "https://software.site/people/luke",
        "https://software.site/people/luke/followers"
    ],
    "instrument": "maintain",
    "target": "https://forge.community/repos/treesim",
    "object": "https://software.site/people/luke",
    "capability": "https://forge.community/repos/treesim/outbox/2NwyPWMX-grant-admin-to-aviva"
}
</xmp>
</div>

And it appears that Luke accepts the invitation:

<div class=example>
<xmp highlight=json-ld>
{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://forgefed.org/ns"
    ],
    "id": "https://software.site/people/luke/activities/mEYYmt8u",
    "type": "Accept",
    "actor": "https://software.site/people/luke",
    "to": [
        "https://forge.community/aviva",
        "https://forge.community/aviva/followers",
        "https://forge.community/repos/treesim",
        "https://forge.community/repos/treesim/followers",
        "https://software.site/people/luke/followers"
    ],
    "object": "https://forge.community/users/aviva/outbox/qfrEGqnC-invite-luke"
}
</xmp>
</div>

Seeing the `Invite` and the `Accept`, the *treesim* repo sends Luke a `Grant`
giving him the access that Aviva offered, and which he accepted:

<div class=example>
<xmp highlight=json-ld>
{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://forgefed.org/ns"
    ],
    "id": "https://forge.community/repos/treesim/outbox/D5uod3pz-grant-maintainer-to-luke",
    "type": "Grant",
    "actor": "https://forge.community/repos/treesim",
    "to": [
        "https://forge.community/aviva",
        "https://forge.community/aviva/followers",
        "https://forge.community/repos/treesim/followers",
        "https://software.site/people/luke",
        "https://software.site/people/luke/followers"
    ],
    "object": "maintain",
    "context": "https://forge.community/repos/treesim",
    "target": "https://software.site/people/luke",
    "fulfills": "https://forge.community/users/aviva/outbox/qfrEGqnC-invite-luke",
    "allows": "invoke",
    "endTime": "2023-12-31T23:00:00-08:00"
}
</xmp>
</div>

Luke can now use this `Grant`, e.g. to delete some old obsolete branch of the
*treesim* repo:

<div class=example>
<xmp highlight=json-ld>
{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://forgefed.org/ns"
    ],
    "id": "https://software.site/people/luke/activities/vShj2aIe",
    "type": "Delete",
    "actor": "https://software.site/people/luke",
    "to": [
        "https://forge.community/repos/treesim",
        "https://forge.community/repos/treesim/followers",
        "https://software.site/people/luke/followers"
    ],
    "object": "https://forge.community/repos/treesim/branches/fixes-for-release-0.1.3",
    "origin": "https://forge.community/repos/treesim",
    "capability": "https://forge.community/repos/treesim/outbox/D5uod3pz-grant-maintainer-to-luke"
}
</xmp>
</div>

Celine requests to have developer access to the *treesim* repo:

<div class=example>
<xmp highlight=json-ld>
{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://forgefed.org/ns"
    ],
    "id": "https://dev.online/@celine/sent/v5Qvd6bB-celine-join",
    "type": "Join",
    "actor": "https://dev.online/@celine",
    "to": [
        "https://forge.community/repos/treesim",
        "https://forge.community/repos/treesim/followers",
        "https://dev.online/@celine/followers"
    ],
    "object": "https://forge.community/repos/treesim",
    "instrument": "write"
}
</xmp>
</div>

Aviva sees the `Join` request, talks with Celine and decides to approve her
request:

<div class=example>
<xmp highlight=json-ld>
{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://forgefed.org/ns"
    ],
    "id": "https://forge.community/users/aviva/outbox/PzRtDydu",
    "type": "Accept",
    "actor": "https://forge.community/users/aviva",
    "to": [
        "https://forge.community/repos/treesim",
        "https://forge.community/repos/treesim/followers",
        "https://dev.online/@celine",
        "https://dev.online/@celine/followers"
    ],
    "object": "https://dev.online/@celine/sent/v5Qvd6bB-celine-join",
    "capability": "https://forge.community/repos/treesim/outbox/2NwyPWMX-grant-admin-to-aviva"
}
</xmp>
</div>

Seeing the `Join` and the `Accept`, the *treesim* repo sends Celine a `Grant`
giving her the access that she requested, and which Aviva approved:

<div class=example>
<xmp highlight=json-ld>
{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://forgefed.org/ns"
    ],
    "id": "https://forge.community/repos/treesim/outbox/D5uod3pz-grant-developer-to-celine",
    "type": "Grant",
    "actor": "https://forge.community/repos/treesim",
    "to": [
        "https://forge.community/aviva",
        "https://forge.community/repos/treesim/followers",
        "https://dev.online/@celine",
        "https://dev.online/@celine/followers"
    ],
    "object": "write",
    "context": "https://forge.community/repos/treesim",
    "target": "https://dev.online/@celine",
    "fulfills": "https://dev.online/@celine/sent/v5Qvd6bB-celine-join",
    "allows": "invoke",
    "endTime": "2023-12-31T23:00:00-08:00"
}
</xmp>
</div>

Celine can now use this `Grant` to access the *treesim* repo.

## Adding and Removing Team Members

Minimal required role: [=admin=]

This is done using the processes described in the [[#granting-access]] section.
In particular, once the new member *M* sends the team *T* the
[[#grant-delegate|delegate-Grant]], *T* SHOULD use it to send *M*
[[#extending-a-delegation-chain|extension-Grants]] of:

- Every authorized (by a valid delegate-Grant) Grant whose [=allows=] is
    [=distribute=], that *T* has received from any of its parent/overseen teams
- Every authorized (by a valid delegate-Grant) Grant whose [=allows=] is
    [=distribute=], that *T* has received from any of the projects it has
    direct access to
- Every authorized (by a valid delegate-Grant) Grant whose [=allows=] is
    [=distribute=], that *T* has received from any of the components it has
    direct access to

## Adding and Removing Project Members

Minimal required role: [=admin=]

This is done using the processes described in the [[#granting-access]] section.
In particular, once the new member *M* sends the project *J* the
[[#grant-delegate|delegate-Grant]], *J* SHOULD use it to send *M*
[[#extending-a-delegation-chain|extension-Grants]] of:

- Every authorized (by a valid delegate-Grant) Grant whose [=allows=] is
    [=gatherAndConvey=], that *J* has received from any of its child projects
- Every authorized (by a valid delegate-Grant) Grant whose [=allows=] is
    [=gatherAndConvey=], that *J* has received from any of its components

## Associating Projects and Components ## {#linking-projects-components}

Minimal required role: [=admin=]

Adding and removing a component to/from a project each have 2 versions: The
"component side" version allows an actor to initiate the action using admin
access to the component, while the "project side" version allows to initiate
the action using admin access to the project.

Whenever authorization is mentioned below, it SHOULD be done using the
[[#s2s-grant-flow|invocation verification process]].

### Adding a component to a project - component side ### {#linking-projects-components-c}

Assuming:

- A project *P*
- A component *C*
- A person with admin access to *C*, Alice
- A person with admin access to *P*, Bob

Alice wants to add component *C* to project *P*. The exchange of activities
SHOULD be as follows:

1. Alice sends an [=Add=] activity in which:
    - [=object=] is *P*
    - [=target=] is the URI of *C*'s [=context=] collection
    - [=instrument=] is the maximal [=Role=] that Alice would like to allow for
        people (and bots) when authorizing their manipulation of *C* by their
        access in *P* (normally it would be [=admin=], perhaps sometimes
        [=maintain=])
    - [=capability=] is the URI of a [=Grant=] that authorizes admin access to
        *C*
2. *C*, seeing and authorizing the Add, publishes an [=Accept=] where
    [=object=] is the Add's URI
3. Bob, seeing the Add and the Accept, sends an [=Accept=] where:
    - [=object=] is the Add's URI
    - [=capability=] is the URI of a [=Grant=] that authorizes admin access to
        *P*
4. *P*, seeing the previous 3 activities and authorizing Bob's Accept,
    publishes a [[#grant-delegate|delegate-Grant]] in which the [=target=] is
    *C*
5. *C*, seeing *P*'s Grant, sends a [[#start-grant-chain|start-Grant]] where:
    - [=actor=] and [=context=] specify *C*
    - [=target=] specifies *P*
    - [=object=] specifies the role specified in the Add's [=instrument=]
    - [=allows=] is [=gatherAndConvey=]
    - [=capability=] is the URI of the delegate-Grant
6. *P*, seeing and authorizing *C*'s Grant, now
    [[#extending-a-delegation-chain|extends the Grant chain]] as relevant, to
    its members and parent projects

### Removing a component from a project - component side ### {#unlinking-projects-components-c}

Assuming:

- A project *P* with a component *C*
- A person with admin access to *C*, Alice

Alice wants to ask component *C* to remove itself from project *P*. The
exchange of activities SHOULD be as follows:

1. Alice sends a [=Remove=] activity where:
    - [=object=] is *P*
    - [=origin=] is the URI of *C*'s [=context=] collection
    - [=capability=] is the URI of a [=Grant=] that authorizes admin access to
        *C*
2. *C*, seeing and authorizing the Remove, publishes a [=Revoke=] where
    [=object=] specifies the active [[#start-grant-chain|start-Grant]] it had
    sent to *P* (and *C* now considers that Grant revoked and no longer
    considers itself as a component of *P*)
3. *P*, seeing the Remove and the Revoke, publishes a [=Revoke=] where
    [=object=] specifies the active [[#grant-delegate|delegate-Grant]] it had
    sent to *C* (and *P* now considers that Grant revoked and no longer
    considers *C* a component of it)

### Adding a component to a project - project side ### {#linking-projects-components-p}

Assuming:

- A project *P*
- A component *C*
- A person with admin access to *C*, Alice
- A person with admin access to *P*, Bob

Bob wants to add component *C* to project *P*. The exchange of activities
SHOULD be as follows:

1. Bob sends an [=Add=] activity in which:
    - [=object=] is *C*
    - [=target=] is the URI of *P*'s [=components=] collection
    - [=instrument=] is the maximal [=Role=] that Bob would like to allow for
        people (and bots) when authorizing their manipulation of *C* by their
        access in *P* (normally it would be [=admin=], perhaps sometimes
        [=maintain=])
    - [=capability=] is the URI of a [=Grant=] that authorizes admin access to
        *P*
2. *P*, seeing and authorizing the Add, publishes an [=Accept=] where
    [=object=] is the Add's URI
3. Alice, seeing the Add and the Accept, sends an [=Accept=] where:
    - [=object=] is the Add's URI
    - [=capability=] is the URI of a [=Grant=] that authorizes admin access to
        *C*
4. *C*, seeing the previous 3 activities and authorizing Alice's Accept, sends
    an [=Accept=] where [=object=] is the Add's URI
5. *P*, seeing *C*'s Accept, publishes a [[#grant-delegate|delegate-Grant]] in
    which the [=target=] is *C*
6. *C*, seeing *P*'s Grant, sends a [[#start-grant-chain|start-Grant]] where:
    - [=actor=] and [=context=] specify *C*
    - [=target=] specifies *P*
    - [=object=] specifies the role specified in the Add's [=instrument=]
    - [=allows=] is [=gatherAndConvey=]
    - [=capability=] is the URI of the delegate-Grant
7. *P*, seeing and authorizing *C*'s Grant, now
    [[#extending-a-delegation-chain|extends the Grant chain]] as relevant, to
    its members and parent projects

### Removing a component from a project - project side ### {#unlinking-projects-components-p}

Assuming:

- A project *P* with a component *C*
- A person with admin access to *P*, Bob

Bob wants to ask project *P* to remove component *C* from it. The exchange of
activities SHOULD be as follows:

1. Bob sends a [=Remove=] activity where:
    - [=object=] is *C*
    - [=origin=] is the URI of *P*'s [=components=] collection
    - [=capability=] is the URI of a [=Grant=] that authorizes admin access to
        *P*
3. *P*, seeing and authorizing the Remove, publishes a [=Revoke=] where
    [=object=] specifies the active [[#grant-delegate|delegate-Grant]] it had
    sent to *C* (and *P* now considers that Grant revoked and no longer
    considers *C* a component of it)
2. *C*, seeing the Remove and the Revoke, publishes a [=Revoke=] where
    [=object=] specifies the active [[#start-grant-chain|start-Grant]] it had
    sent to *P* (and *C* now considers that Grant revoked and no longer
    considers itself as a component of *P*)

## Adding and Removing Children and Parents to Projects and Teams ## {#linking-parent-child}

Minimal required role: [=admin=]

Like most cooperation between ForgeFed actors, the parent-child link is based
on mutual consent:

- To form the link, both projects/teams must explicitly agree to it
- To remove the link, it's enough for one of them to withdraw its consent

Parent-child links are a part of the Grant delegation system, in which the flow
is:

- Components initiate delegation, to:
    - The projects they belong to
    - Teams which have access to them
- Projects delegate to their:
    - Direct collaborators
    - Parent projects
    - Teams which have access to them
- Teams delegate to their:
    - Direct collaborators
    - Child teams

Once a parent-child link is formed, delegations start happening through the
link. This is described in more detail below.

Each delegation within this flow therefore has a "source" side, which sends the
delegation, and a "destination" side, which receives it and possibly delegates
it further.

In a parent-child link, therefore, there is a "source" side and a "destination"
side. For projects, the child is the source and the parent is the destination
(because projects delegate to their parents, once a link is formed). For teams,
the parent is the source and the child is the destination (because teams
delegate to their children, once a link is formed).

Since either side of the link can initiate the process, and since delegation
flow occurs in one direction, there are essentially two versions of the
process:

- Process initiated by the source side (i.e. a project asking to have a parent,
    or a team asking to have a child)
- Process initiated by the destination side (i.e. a project asking to have a
    child, or a team asking to have a parent)

In the [=Add=] activity that initiates the process, the initiating side is
determined by the [=target=] property. The actor to whom the collection
specified by [=target=] belongs, is the initiating side. The other side of the
link is specified in the [=object=] property.

Whenever authorization is mentioned below, it SHOULD be done using the
[[#s2s-grant-flow|invocation verification process]].

### Forming a project child-parent link ### {#linking-parent-child-p}

#### Initiated by the parent #### {#linking-parent-child-p-p}

Assuming:

- A project *P*
- A project *C*
- A person with admin access to *C*, Celia
- A person with admin access to *P*, Philip

Philip wants *C* to become a child of *P* (which means *P* will be a parent
of *C*). The exchange of activities SHOULD be as follows:

1. Philip sends an [=Add=] activity in which:
    - [=object=] is *C*
    - [=target=] is the URI of *P*'s [=subprojects=] (i.e. children) collection
    - [=instrument=] is the maximal [=Role=] that Philip would like to allow
        for people (and bots) when authorizing their manipulation of *C* by
        their access in *P* (normally it would be [=admin=], perhaps sometimes
        [=maintain=])
    - [=capability=] is the URI of a [=Grant=] that authorizes Philip's admin
        access to *P*
2. *P*, seeing and authorizing the Add, publishes an [=Accept=] where
    [=object=] is the Add's URI
3. Celia, seeing the Add and the Accept, sends an [=Accept=] where:
    - [=object=] is the Add's URI
    - [=capability=] is the URI of a [=Grant=] that authorizes Celia's admin
        access to *C*
4. *C*, seeing the Add and authorizing Celia's Accept, publishes an [=Accept=]
    where [=object=] is the Add's URI
5. *P*, seeing *C*'s Accept, publishes a [[#grant-delegate|delegate-Grant]] in
    which the [=target=] is *C*, and now considers *C* a child and lists *C* in
    the [=subprojects=] collection
6. *C*, seeing *P*'s Grant, now considers *P* a parent and lists *P* in the
    [=context=] (i.e. parents) collection

Now *C* sends delegation Grant activities, see the [[#deleg-step-j]] section
below.

#### Initiated by the child #### {#linking-parent-child-p-c}

Assuming:

- A project *P*
- A project *C*
- A person with admin access to *C*, Celia
- A person with admin access to *P*, Philip

Celia wants *C* to become a child of *P* (which means *P* will be a parent
of *C*). The exchange of activities SHOULD be as follows:

1. Celia sends an [=Add=] activity in which:
    - [=object=] is *P*
    - [=target=] is the URI of *C*'s [=context=] (i.e. parents) collection
    - [=instrument=] is the maximal [=Role=] that Celia would like to allow
        for people (and bots) when authorizing their manipulation of *C* by
        their access in *P* (normally it would be [=admin=], perhaps sometimes
        [=maintain=])
    - [=capability=] is the URI of a [=Grant=] that authorizes Celia's admin
        access to *C*
2. *C*, seeing and authorizing the Add, publishes an [=Accept=] where
    [=object=] is the Add's URI
3. Philip, seeing the Add and the Accept, sends an [=Accept=] where:
    - [=object=] is the Add's URI
    - [=capability=] is the URI of a [=Grant=] that authorizes Philip's admin
        access to *P*
4. *P*, seeing the Add, seeing *C*'s Accept, and authorizing Philip's Accept,
    publishes a [[#grant-delegate|delegate-Grant]] in which the [=target=] is
    *C*, and now considers *C* a child and lists *C* in the [=subprojects=]
    collection
5. *C*, seeing *P*'s Grant, now considers *P* a parent and lists *P* in the
    [=context=] (i.e. parents) collection

Now *C* sends delegation Grant activities, see the [[#deleg-step-j]] section
below.

#### Delegation step #### {#deleg-step-j}

*C* now sends Grant activities, which *P* delegates further by sending its own
Grant activities, as described in [[#extending-a-delegation-chain]], to its
member humans, member teams, and parent projects.

1. *C* sends a [[#start-grant-chain|start-Grant]] where:
    - [=actor=] and [=context=] specify *C*
    - [=target=] specifies *P*
    - [=object=] specifies the role specified in the Add's [=instrument=]
    - [=allows=] is [=gatherAndConvey=]
    - [=capability=] is the URI of the delegate-Grant from step 5 or 4 above
2. For each delegation Grant *g* that *C* has received from its components and
    child projects, *C* sends an
    [[#extending-a-delegation-chain|extension-Grant]] where:
        - [=actor=] specifies *C*
        - [=context=] is identical to *g*'s [=context=]
        - [=target=] specifies *P*
        - [=object=] specifies the lower role among the one specified by the
            Add's [=instrument=], and the one specified by *g*'s [=object=]
        - [=allows=] is [=gatherAndConvey=]
        - [=capability=] is the URI of the delegate-Grant from step 5 or 4
            above
        - [=delegates=] specifies *g*'s URI

When *P* sees these Grants, it delegates them further, as mentioned above.

In addition, as long as the parent-child link remains active, whenever *C*
receives a delegation Grant from any of its components or children, it SHOULD
send *P* an extension-Grant as described above, and *P* SHOULD extend it
further.

### Removing a project child-parent link ### {#unlinking-parent-child-p}

#### Initiated by the child #### {#unlinking-parent-child-p-c}

Assuming:

- Projects *P* and *C*, where *P* is a parent of *C*
- A person with admin access to *C*, Celia

Celia wants *C* to stop being a child of *P* (which means *P* will stop
being a parent of *C*). The exchange of activities SHOULD be as follows:

1. Celia sends an [=Remove=] activity in which:
    - [=object=] is *P*
    - [=origin=] is the URI of *C*'s [=context=] (i.e. parents) collection
    - [=capability=] is the URI of a [=Grant=] that authorizes Celia's admin
        access to *C*
2. *C*, seeing and authorizing the Remove, publishes an [=Accept=] where
    [=object=] is the Add's URI
3. *P*, seeing the Remove and the Accept, publishes a [=Revoke=] activity in
    which the [=object=] is the delegate-Grant it had sent to *C*, and now
    considers that Grant revoked, and removes *C* from its [=subprojects=]
    collection
4. *C*, seeing *P*'s Revoke, removes *P* from its [=context=] (i.e. parents)
    collection

Now, for each Grant *g* that *P* has published, as an extension of a Grant it
had received from *C*:

1. *P* sends (to *g*'s [=target=]) a [=Revoke=] activity whose [=object=]
    specifies *g*'s URI
2. *P* considers *g* invalidated
3. The recipient of the [=Revoke=], if it further published extensions of *g*,
    now similarly publishes Revoke activities and considers those extension
    Grants invalid

#### Initiated by the parent #### {#unlinking-parent-child-p-p}

Assuming:

- Projects *P* and *C*, where *P* is a parent of *C*
- A person with admin access to *P*, Philip

Philip wants *C* to stop being a child of *P* (which means *P* will stop
being a parent of *C*). The exchange of activities SHOULD be as follows:

1. Philip sends an [=Remove=] activity in which:
    - [=object=] is *C*
    - [=origin=] is the URI of *P*'s [=subprojects=] (i.e. children) collection
    - [=capability=] is the URI of a [=Grant=] that authorizes Philip's admin
        access to *P*
2. *P*, seeing and authorizing the Remove, publishes a [=Revoke=] activity in
    which the [=object=] is the delegate-Grant it had sent to *C*, and now
    considers that Grant revoked, and removes *C* from its [=subprojects=]
    collection
3. *C*, seeing *P*'s Revoke, removes *P* from its [=context=] (i.e. parents)
    collection, and MAY publish an Accept (whose [=object=] is the Remove) to
    notify its followers

Now, for each Grant *g* that *P* has published, as an extension of a Grant it
had received from *C*:

1. *P* sends (to *g*'s [=target=]) a [=Revoke=] activity whose [=object=]
    specifies *g*'s URI
2. *P* considers *g* invalidated
3. The recipient of the [=Revoke=], if it further published extensions of *g*,
    now similarly publishes Revoke activities and considers those extension
    Grants invalid

### Forming a team child-parent link ### {#linking-parent-child-t}

#### Initiated by the parent #### {#linking-parent-child-t-p}

Assuming:

- A team *P*
- A team *C*
- A person with admin access to *C*, Celia
- A person with admin access to *P*, Philip

Philip wants *C* to become a child of *P* (which means *P* will be a parent
of *C*). The exchange of activities SHOULD be as follows:

1. Philip sends an [=Add=] activity in which:
    - [=object=] is *C*
    - [=target=] is the URI of *P*'s [=subteams=] (i.e. children) collection
    - [=instrument=] is the maximal [=Role=] that Philip would like to allow
        for people (and bots) when authorizing their manipulation of *P* by
        their access in *C* (normally it would be [=admin=], perhaps sometimes
        [=maintain=])
    - [=capability=] is the URI of a [=Grant=] that authorizes Philip's admin
        access to *P*
2. *P*, seeing and authorizing the Add, publishes an [=Accept=] where
    [=object=] is the Add's URI
3. Celia, seeing the Add and the Accept, sends an [=Accept=] where:
    - [=object=] is the Add's URI
    - [=capability=] is the URI of a [=Grant=] that authorizes Celia's admin
        access to *C*
4. *C*, seeing the Add, seeing *P*'s Accept, and authorizing Celia's Accept,
    publishes a [[#grant-delegate|delegate-Grant]] in which the [=target=] is
    *P*, and now considers *P* a parent and lists *P* in the [=context=] (i.e.
    parents) collection
5. *P*, seeing *C*'s Grant, now considers *C* a parent and lists *C* in the
    [=subteams=] collection

Now *P* sends delegation Grant activities, see the [[#deleg-step-t]] section
below.

#### Initiated by the child #### {#linking-parent-child-t-c}

Assuming:

- A team *P*
- A team *C*
- A person with admin access to *C*, Celia
- A person with admin access to *P*, Philip

Celia wants *C* to become a child of *P* (which means *P* will be a parent
of *C*). The exchange of activities SHOULD be as follows:

1. Celia sends an [=Add=] activity in which:
    - [=object=] is *P*
    - [=target=] is the URI of *C*'s [=context=] (i.e. parents) collection
    - [=instrument=] is the maximal [=Role=] that Celia would like to allow
        for people (and bots) when authorizing their manipulation of *C* by
        their access in *P* (normally it would be [=admin=], perhaps sometimes
        [=maintain=])
    - [=capability=] is the URI of a [=Grant=] that authorizes Celia's admin
        access to *C*
2. *C*, seeing and authorizing the Add, publishes an [=Accept=] where
    [=object=] is the Add's URI
3. Philip, seeing the Add and the Accept, sends an [=Accept=] where:
    - [=object=] is the Add's URI
    - [=capability=] is the URI of a [=Grant=] that authorizes Philip's admin
        access to *P*
4. *P*, seeing the Add, seeing *C*'s Accept, and authorizing Philip's Accept,
    publishes an [=Accept=] where [=object=] is the Add's URI
5. *C*, seeing *P*'s Accept, publishes a [[#grant-delegate|delegate-Grant]] in
    which the [=target=] is *P*, and now considers *P* a parent and lists *P*
    in the [=context=] (i.e. parents) collection
6. *P*, seeing *C*'s Grant, now considers *C* a child and lists *C* in the
    [=subteams=] collection

Now *P* sends delegation Grant activities, see the [[#deleg-step-t]] section
below.

#### Delegation step #### {#deleg-step-t}

*P* now sends Grant activities, which *C* delegates further by sending its own
Grant activities, as described in [[#extending-a-delegation-chain]], to its
member humans and child teams.

For each delegation Grant *g* that *P* has received from its projects,
components, parent teams and overseen teams, *P* sends an
[[#extending-a-delegation-chain|extension-Grant]] where:

- [=actor=] specifies *P*
- [=context=] is identical to *g*'s [=context=]
- [=target=] specifies *C*
- [=object=] specifies the lower role among the one specified by the Add's
    [=instrument=], and the one specified by *g*'s [=object=]
- [=allows=] is [=distribute=]
- [=capability=] is the URI of the delegate-Grant from step 4 or 5 above
- [=delegates=] specifies *g*'s URI

When *C* sees these Grants, it delegates them further, as mentioned above.

In addition, as long as the parent-child link remains active, whenever *P*
receives a delegation Grant from any of its projects, components or parents, it
SHOULD send *C* an extension-Grant as described above, and *C* SHOULD extend it
further.

### Removing a team child-parent link ### {#unlinking-parent-child-t}

#### Initiated by the child #### {#unlinking-parent-child-t-c}

Assuming:

- Teams *P* and *C*, where *P* is a parent of *C*
- A person with admin access to *C*, Celia

Celia wants *C* to stop being a child of *P* (which means *P* will stop
being a parent of *C*). The exchange of activities SHOULD be as follows:

1. Celia sends an [=Remove=] activity in which:
    - [=object=] is *P*
    - [=origin=] is the URI of *C*'s [=context=] (i.e. parents) collection
    - [=capability=] is the URI of a [=Grant=] that authorizes Celia's admin
        access to *C*
2. *C*, seeing and authorizing the Remove, publishes a [=Revoke=] activity in
    which the [=object=] is the delegate-Grant it had sent to *P*, and now
    considers that Grant revoked, and removes *P* from its [=context=] (i.e.
    parents) collection
3. *P*, seeing *C*'s Revoke, removes *C* from its [=subteams=] collection,
    and MAY publish an Accept (whose [=object=] is the Remove) to notify its
    followers

Now, for each Grant *g* that *C* has published, as an extension of a Grant it
had received from *P*:

1. *C* sends (to *g*'s [=target=]) a [=Revoke=] activity whose [=object=]
    specifies *g*'s URI
2. *C* considers *g* invalidated
3. The recipient of the [=Revoke=], if it further published extensions of *g*,
    now similarly publishes Revoke activities and considers those extension
    Grants invalid

#### Initiated by the parent #### {#unlinking-parent-child-t-p}

Assuming:

- Teams *P* and *C*, where *P* is a parent of *C*
- A person with admin access to *P*, Philip

Philip wants *C* to stop being a child of *P* (which means *P* will stop
being a parent of *C*). The exchange of activities SHOULD be as follows:

1. Philip sends an [=Remove=] activity in which:
    - [=object=] is *C*
    - [=origin=] is the URI of *P*'s [=subteams=] (i.e. parents) collection
    - [=capability=] is the URI of a [=Grant=] that authorizes Philip's admin
        access to *P*
2. *P*, seeing and authorizing the Remove, publishes an [=Accept=] where
    [=object=] is the Add's URI
3. *C*, seeing the Remove and the Accept, publishes a [=Revoke=] activity in
    which the [=object=] is the delegate-Grant it had sent to *P*, and now
    considers that Grant revoked, and removes *P* from its [=context=] (i.e.
    parents) collection
4. *P*, seeing *C*'s Revoke, removes *C* from its [=subteams=] collection

Now, for each Grant *g* that *C* has published, as an extension of a Grant it
had received from *P*:

1. *C* sends (to *g*'s [=target=]) a [=Revoke=] activity whose [=object=]
    specifies *g*'s URI
2. *C* considers *g* invalidated
3. The recipient of the [=Revoke=], if it further published extensions of *g*,
    now similarly publishes Revoke activities and considers those extension
    Grants invalid

### Forming a team oversight link ### {#linking-oversight}

#### Initiated by the overseen side #### {#linking-oversight-p}

Assuming:

- A team *P*
- A team *C*
- A person with admin access to *C*, Celia
- A person with admin access to *P*, Philip

Philip wants *C* to become an oversight team of *P* (which means *P* will
overseen by *C*). The exchange of activities SHOULD be as follows:

1. Philip sends an [=Add=] activity in which:
    - [=object=] is *C*
    - [=target=] is the URI of *P*'s [=overseenBy=] collection
    - [=instrument=] is the maximal [=Role=] that Philip would like to allow
        for people (and bots) when authorizing their manipulation of *P* by
        their access in *C*
    - [=capability=] is the URI of a [=Grant=] that authorizes Philip's admin
        access to *P*
2. *P*, seeing and authorizing the Add, publishes an [=Accept=] where
    [=object=] is the Add's URI
3. Celia, seeing the Add and the Accept, sends an [=Accept=] where:
    - [=object=] is the Add's URI
    - [=capability=] is the URI of a [=Grant=] that authorizes Celia's admin
        access to *C*
4. *C*, seeing the Add, seeing *P*'s Accept, and authorizing Celia's Accept,
    publishes a [[#grant-delegate|delegate-Grant]] in which the [=target=] is
    *P*, and now considers *P* an overseen team and lists *P* in the
    [=oversees=] collection
5. *P*, seeing *C*'s Grant, now considers *C* an overseeing team and lists *C*
    in the [=overseenBy=] collection

Now *P* sends a delegation Grant activity, see the [[#deleg-step-o]] section
below.

#### Initiated by the overseeing side #### {#linking-oversight-c}

Assuming:

- A team *P*
- A team *C*
- A person with admin access to *C*, Celia
- A person with admin access to *P*, Philip

Celia wants *C* to oversee *P* (which means *P* will be overseen by *C*). The
exchange of activities SHOULD be as follows:

1. Celia sends an [=Add=] activity in which:
    - [=object=] is *P*
    - [=target=] is the URI of *C*'s [=oversees=] collection
    - [=instrument=] is the maximal [=Role=] that Celia would like to allow
        for people (and bots) when authorizing their manipulation of *P* by
        their access in *C*
    - [=capability=] is the URI of a [=Grant=] that authorizes Celia's admin
        access to *C*
2. *C*, seeing and authorizing the Add, publishes an [=Accept=] where
    [=object=] is the Add's URI
3. Philip, seeing the Add and the Accept, sends an [=Accept=] where:
    - [=object=] is the Add's URI
    - [=capability=] is the URI of a [=Grant=] that authorizes Philip's admin
        access to *P*
4. *P*, seeing the Add, seeing *C*'s Accept, and authorizing Philip's Accept,
    publishes an [=Accept=] where [=object=] is the Add's URI
5. *C*, seeing *P*'s Accept, publishes a [[#grant-delegate|delegate-Grant]] in
    which the [=target=] is *P*, and now considers *P* an overseen team and
    lists *P* in the [=oversees=] collection
6. *P*, seeing *C*'s Grant, now considers *C* an overseeing team and lists *C*
    in the [=overseenBy=] collection

Now *P* sends a delegation Grant activity, see the [[#deleg-step-o]] section
below.

#### Delegation step #### {#deleg-step-o}

*P* now sends a Grant activity, which *C* delegates further by sending its own
Grant activities, as described in [[#extending-a-delegation-chain]], to its
member humans and child teams.

*P* sends a [[#start-grant-chain|start-Grant]] where:

- [=actor=] and [=context=] specify *P*
- [=target=] specifies *C*
- [=object=] specifies the role specified in the Add's [=instrument=]
- [=allows=] is [=distribute=]
- [=capability=] is the URI of the delegate-Grant from step 4 or 5 above

When *C* sees this Grant, it delegates it further, as mentioned above.

### Removing a team oversight link ### {#unlinking-oversight}

#### Initiated by the overseeing side #### {#unlinking-oversight-c}

Assuming:

- Teams *P* and *C*, where *P* is overseen by *C*
- A person with admin access to *C*, Celia

Celia wants *C* to stop overseeing *P* (which means *P* will stop
being overseen by *C*). The exchange of activities SHOULD be as follows:

1. Celia sends an [=Remove=] activity in which:
    - [=object=] is *P*
    - [=origin=] is the URI of *C*'s [=oversees=] collection
    - [=capability=] is the URI of a [=Grant=] that authorizes Celia's admin
        access to *C*
2. *C*, seeing and authorizing the Remove, publishes a [=Revoke=] activity in
    which the [=object=] is the delegate-Grant it had sent to *P*, and now
    considers that Grant revoked, and removes *P* from its [=oversees=]
    collection
3. *P*, seeing *C*'s Revoke, removes *C* from its [=overseenBy=] collection,
    and MAY publish an Accept (whose [=object=] is the Remove) to notify its
    followers

Now, for each Grant *g* that *C* has published, as an extension of the
start-Grant it had received from *P*:

1. *C* sends (to *g*'s [=target=]) a [=Revoke=] activity whose [=object=]
    specifies *g*'s URI
2. *C* considers *g* invalidated
3. The recipient of the [=Revoke=], if it further published extensions of *g*,
    now similarly publishes Revoke activities and considers those extension
    Grants invalid

#### Initiated by the overseen side #### {#unlinking-oversight-p}

Assuming:

- Teams *P* and *C*, where *P* is overseen by *C*
- A person with admin access to *P*, Philip

Philip wants *C* to stop overseeing *P* (which means *P* will stop being
overseen by *C*). The exchange of activities SHOULD be as follows:

1. Philip sends an [=Remove=] activity in which:
    - [=object=] is *C*
    - [=origin=] is the URI of *P*'s [=overseenBy=] collection
    - [=capability=] is the URI of a [=Grant=] that authorizes Philip's admin
        access to *P*
2. *P*, seeing and authorizing the Remove, publishes an [=Accept=] where
    [=object=] is the Add's URI
3. *C*, seeing the Remove and the Accept, publishes a [=Revoke=] activity in
    which the [=object=] is the delegate-Grant it had sent to *P*, and now
    considers that Grant revoked, and removes *P* from its [=oversees=]
    collection
4. *P*, seeing *C*'s Revoke, removes *C* from its [=overseenBy=] collection

Now, for each Grant *g* that *C* has published, as an extension of the
start-Grant it had received from *P*:

1. *C* sends (to *g*'s [=target=]) a [=Revoke=] activity whose [=object=]
    specifies *g*'s URI
2. *C* considers *g* invalidated
3. The recipient of the [=Revoke=], if it further published extensions of *g*,
    now similarly publishes Revoke activities and considers those extension
    Grants invalid

## Enabling and Disabling Team Access to a Resource

Minimal required role: [=admin=]

As described in previous sections, resource actors (Teams, Projects and
components) can have direct collaborators (usually Persons and utility bots),
through the [[#granting-access|direct granting]] process. [=Team=] actors,
however, have a special kind of being collaborators: Rather than receiving a
Grant they can invoke themselves, they receive Grants that they delegate to
their members, and recursively delegate to their child teams.

This kind of collaboration link is described in this section.

Like most cooperation between ForgeFed actors, the link is based on mutual
consent:

- To form the link, both the team and the resource must explicitly agree to it
- To remove the link, it's enough for one of them to withdraw its consent

These links are a part of the Grant delegation system, in which the flow is:

- Components initiate delegation, to:
    - The projects they belong to
    - **Teams which have access to them**
- Projects delegate to their:
    - Direct collaborators
    - Parent projects
    - **Teams which have access to them**
- **Teams delegate to their:**
    - **Direct collaborators**
    - **Child teams**

Once a link is formed, delegations start happening through the link. This is
described in more detail below.

Since either side of the link can initiate the process, and since delegation
flow occurs in one direction, there are essentially two versions of the
process:

- Process initiated by the resource side (i.e. a resource asking to give access
    a team)
- Process initiated by the team side (i.e. a team asking to have access to a
    resource)

In the [=Add=] activity that initiates the process, the initiating side is
determined by the [=target=] property. The actor to whom the collection
specified by [=target=] belongs, is the initiating side. The other side of the
link, receiving the Add, is specified in the [=object=] property.

Whenever authorization is mentioned below, it SHOULD be done using the
[[#s2s-grant-flow|invocation verification process]].

### Forming a team-resource link ### {#forming-team-resource-link}

#### Initiated by the resource #### {#forming-team-resource-link-r}

Assuming:

- A team *T*
- A resource actor *R*
- A person with admin access to *T*, Tessa
- A person with admin access to *R*, Robin

Robin wants *T* to gain access to *R*. The exchange of activities SHOULD be as
follows:

1. Robin sends an [=Add=] activity in which:
    - [=object=] is *T*
    - [=target=] is the URI of *R*'s [=teams=] collection
    - [=instrument=] is the maximal [=Role=] that Robin would like to allow
        for people (and bots) when authorizing their manipulation of *R* by
        their access in *T* (normally it would be [=admin=], perhaps sometimes
        [=maintain=])
    - [=capability=] is the URI of a [=Grant=] that authorizes Robin's admin
        access to *R*
2. *R*, seeing and authorizing the Add, publishes an [=Accept=] where
    [=object=] is the Add's URI
3. Tessa, seeing the Add and the Accept, sends an [=Accept=] where:
    - [=object=] is the Add's URI
    - [=capability=] is the URI of a [=Grant=] that authorizes Celia's admin
        access to *T*
4. *T*, seeing the Add, seeing *R*'s Accept, and authorizing Tessa's Accept,
    publishes a [[#grant-delegate|delegate-Grant]] in which the [=target=] is
    *R*, and now considers *R* an accessible resource and lists *R* in the
    [=teamResources=] collection
5. *R*, seeing *T*'s Grant, now considers *R* a collaborator and lists *R* in
    the [=teams=] collection

Now *R* sends delegation Grant activities, see the [[#deleg-step-tr]] section
below.

#### Initiated by the team #### {#forming-team-resource-link-t}

Assuming:

- A team *T*
- A resource actor *R*
- A person with admin access to *T*, Tessa
- A person with admin access to *R*, Robin

Tessa wants *T* to gain access to *R*. The exchange of activities SHOULD be as
follows:

1. Tessa sends an [=Add=] activity in which:
    - [=object=] is *R*
    - [=target=] is the URI of *T*'s [=teamResources=] collection
    - [=instrument=] is the maximal [=Role=] that Tessa would like to have and
        allow for people (and bots) when authorizing their manipulation of *R*
        by their access in *T* (normally it would be [=admin=], perhaps
        sometimes [=maintain=])
    - [=capability=] is the URI of a [=Grant=] that authorizes Tessa's admin
        access to *T*
2. *T*, seeing and authorizing the Add, publishes an [=Accept=] where
    [=object=] is the Add's URI
3. Robin, seeing the Add and the Accept, sends an [=Accept=] where:
    - [=object=] is the Add's URI
    - [=capability=] is the URI of a [=Grant=] that authorizes Robin's admin
        access to *R*
4. *R*, seeing the Add, seeing *T*'s Accept, and authorizing Robin's Accept,
    publishes an [=Accept=] where [=object=] is the Add's URI
5. *T*, seeing *R*'s Accept, publishes a [[#grant-delegate|delegate-Grant]] in
    which the [=target=] is *R*, and now considers *R* an accessible resource
    and lists *R* in the [=teamResources=] collection
6. *R*, seeing *T*'s Grant, now considers *T* a collaborator and lists *T* in
    the [=teams=] collection

Now *R* sends delegation Grant activities, see the [[#deleg-step-tr]] section
below.

#### Delegation step #### {#deleg-step-tr}

*R* now sends Grant activities, which *T* delegates further by sending its own
Grant activities, as described in [[#extending-a-delegation-chain]], to its
member humans and child teams.

1. *R* sends a [[#start-grant-chain|start-Grant]] where:
    - [=actor=] and [=context=] specify *R*
    - [=target=] specifies *T*
    - [=object=] specifies the role specified in the Add's [=instrument=]
    - [=allows=] is [=distribute=]
    - [=capability=] is the URI of the delegate-Grant from step 4 or 5 above
2. If *R* is a Project, then for each delegation Grant *g* that *R* has
    received from its components or children, *R* sends an
    [[#extending-a-delegation-chain|extension-Grant]] where:
    - [=actor=] specifies *R*
    - [=context=] is identical to *g*'s [=context=]
    - [=target=] specifies *T*
    - [=object=] specifies the lower role among the one specified by the
        Add's [=instrument=], and the one specified by *g*'s [=object=]
    - [=allows=] is [=distribute=]
    - [=capability=] is the URI of the delegate-Grant from step 4 or 5 above
    - [=delegates=] specifies *g*'s URI

When *T* sees these Grants, it delegates them further, as mentioned above.

In addition, as long as the team-resource link remains active, whenever *R*
receives a delegation Grant from any of its components or children, it SHOULD
send *T* an extension-Grant as described above, and *T* SHOULD extend it
further.

### Removing a team-resource link ### {#removing-team-resource-link}

#### Initiated by the resource #### {#removing-team-resource-link-r}

Assuming:

- A team *T*, that has access to a resource actor *R*
- A person with admin access to *R*, Robin

Robin wants *T* to stop being a collaborator in *R*. The exchange of activities
SHOULD be as follows:

1. Robin sends a [=Remove=] activity in which:
    - [=object=] is *T*
    - [=origin=] is the URI of *T*'s [=teams=] collection
    - [=capability=] is the URI of a [=Grant=] that authorizes Robin's admin
        access to *R*
2. *R*, seeing and authorizing the Remove, publishes an [=Accept=] where
    [=object=] is the Remove's URI, and *R* removes *T* from its [=teams=]
    collection
3. *T*, seeing the Remove and the Accept, publishes a [=Revoke=] activity in
    which the [=object=] is the delegate-Grant it had sent to *R*, and now
    considers that Grant revoked, and removes *R* from its [=teamResources=]
    collection

Now, for each Grant *g* that *T* has published, as an extension of a Grant it
had received from *R*:

1. *T* sends (to *g*'s [=target=]) a [=Revoke=] activity whose [=object=]
    specifies *g*'s URI
2. *T* considers *g* invalidated
3. The recipient of the [=Revoke=], if it further published extensions of *g*,
    now similarly publishes Revoke activities and considers those extension
    Grants invalid

#### Initiated by the team #### {#removing-team-resource-link-t}

Assuming:

- A team *T*, that has access to a resource actor *R*
- A person with admin access to *T*, Tessa

Tessa wants *T* to stop being a collaborator in *R*. The exchange of activities
SHOULD be as follows:

1. Tessa sends a [=Remove=] activity in which:
    - [=object=] is *R*
    - [=origin=] is the URI of *T*'s [=teamResources=] collection
    - [=capability=] is the URI of a [=Grant=] that authorizes Tessa's admin
        access to *T*
2. *T*, seeing and authorizing the Remove, publishes a [=Revoke=] activity in
    which the [=object=] is the delegate-Grant it had sent to *R*, and now
    considers that Grant revoked, and removes *R* from its [=teamResources=]
    collection
3. *R*, seeing *T*'s Revoke, removes *T* from its [=teams=] collection, and MAY
    publish an Accept (whose [=object=] is the Remove) to notify its followers

Now, for each Grant *g* that *T* has published, as an extension of a Grant it
had received from *R*:

1. *T* sends (to *g*'s [=target=]) a [=Revoke=] activity whose [=object=]
    specifies *g*'s URI
2. *T* considers *g* invalidated
3. The recipient of the [=Revoke=], if it further published extensions of *g*,
    now similarly publishes Revoke activities and considers those extension
    Grants invalid

## Creating Resource Actors

Minimal required role: [=write=]

There are 2 types of actors whose process of creation is a special case:

1. [=Person=]: Created by registering an account
2. [=Factory=]: Can be created by a C2S [=Create=] activity, where the access
    control to this operation is server-specific (for example perhaps only
    server administrators can create Factories)

All the other actor types, the resouce actors besides Factory, MUST be created
via a [=Factory=] actor.

- [=Repository=]
- [=TicketTracker=]
- [=PatchTracker=]
- [=Workflow=]
- [=Roadmap=]
- [=ReleaseTracker=]
- [=Project=]
- [=Team=]
- [=Organization=]

A given factory specifies via the [=availableActorTypes=] the specific types it
supports.

Assuming:

- A factory *F*
- A person with [=write=] access to *F*, Philip

Philip wants to create a new actor, of a type supported by *F*. The exchange of
activities SHOULD be as follows:

1. Philip sends a [=Create=] activity in which:
    - [=object=] contains the properties of the new actor to be created
    - [=origin=] is the URI of *F*
    - [=capability=] is the URI of a [=Grant=] that authorizes Philip's access
        to *F*
2. *F*, seeing and authorizing the Create, publishes an [=Accept=] where
    [=object=] is the Create's URI and [=result=] is the URI of the newly
    created actor *A*
3. *A*, upon its creation, publishes a [[#direct-granting|direct-Grant]] in
    which the [=target=] is Philip and [=object=] is [=admin=], thus giving
    Philip full access to manage *A*

## Deleting Resource Actors

Minimal required role: [=admin=]

Given:

- A resource actor *R*, i.e. an actor whose type is one of:
    - [=Factory=]
    - [=Repository=]
    - [=TicketTracker=]
    - [=PatchTracker=]
    - [=Workflow=]
    - [=Roadmap=]
    - [=ReleaseTracker=]
    - [=Project=]
    - [=Team=]
    - [=Organization=]
- A person with [=admin=] access to *R*, Philip

*R* can be deleted via the following sequence:

1. Philip sends a [=Delete=] activity in which:
    - [=object=] is the URI of *R*
    - [=capability=] is the URI of a [=Grant=] that authorizes Philip's
        [=admin=] access to *R*
2. *R* sends, to its followers and to Philip, an [=Accept=] activity whose
    [=object=] is the URI of the [=Delete=]
3. *R* deletes itself, such that further GET requests for *R* or any of its
    objects return 404 or 410 status

## Editing an Object ## {#editing}

Minimal required role: It varies

The problem with the [=Update=] activity is that it contains a full description
of the updated object. This means it's not a successful candidate for
describing clear atomic change requests. Since there is no standard solution on
the Fediverse at the time of writing, ForgeFed for now introduces its own
solution:

A new actvity type, [=Edit=].

It is intentionally not named Patch, as that name is already in use in ForgeFed
under a different meaning. See [=Patch=].

NOTE: Each resource type specifies the fields that it allows actors to edit via
the [=Edit=] activity. For example, for a [=Note=] this would be the [=source=]
and [=content=].

Assuming:

- A resource actor *R*,
- A resource *r* managed by *R*
- A person with edit access to *r*, Celia

Celia wants to edit certain properties of *r*, which are meant to be edited
using the [=Edit=] activity. The exchange of activities SHOULD be as follows:

1. Celia sends an [=Edit=] activity in which:
    - [=object=] is an object in which:
        - [=id=] is the URI of *r*
        - The other fields are the ones Celia wants to set to new values
    - [=context=] is the URI of *R*
    - (optionally) [=capability=] is the URI of a [=Grant=] that authorizes
        Celia's edit access to *r*
2. *R*, seeing the Edit and authorizing Celia's access and changes, publishes
    an [=Accept=] where [=object=] is the Edit's URI, and updates the
    representation of *r* that it serves from now on

Here is a specification of object types and the properties they allow to change
via [=Edit=].

### Edit a comment's text

<pre class=simpledef>
Type: [=Note=]
Properties: [=source=], [=content=]
Role: None required
Other requirements: An actor can edit only a Note they've authored
</pre>

### Edit a Ticket's title and description ### {#editing-ticket}

<pre class=simpledef>
Type: [=Ticket=]
Properties: [=summary=], [=source=], [=content=]
Role: For the author not required, otherwise [=triage=]
Other requirements: None
</pre>

### Edit a Ticket's milestone ### {#s2s-edit-ticket-milestone}

<pre class=simpledef>
Type: [=Ticket=]
Properties: [=ticketMilestone=]
Role: [=triage=]
Other requirements: None
</pre>

### Edit a Ticket's custom fields ### {#editing-ticket-custom}

<pre class=simpledef>
Type: [=Ticket=]
Properties: [=customFields=]
Role: [=triage=]
Other requirements:
    1. Allowed only if the tracker has an associated [=Workflow=]
    2. Note that this Edit is special! Rather than setting the [=customFields=]
        property, it *edits* it. [=customFields=] MUST specify here 1 or more
        objects in which [=type=] is [=FieldValue=], [=context=] refers to a
        [=Field=] and [=prop:fieldValue=] is either a value matching that
        field's type (which indicates setting the field) or `null` (which
        indicates removing the field).
</pre>

### Edit a Factory's list of available actor types ### {#editing-factory-types}

<pre class=simpledef>
Type: [=Factory=]
Properties: [=availableActorTypes=]
Role: [=admin=]
Other requirements:
    Each specified type MUST be a type this Factory is programmed to support
</pre>

### Edit a Enum Value's name, description, color ### {#editing-enum-value}

<pre class=simpledef>
Type: [=EnumValue=]
Properties: [=name=], [=summary=], [=enumValueColor=]
Role: [=maintain=]
Other requirements: [=name=] must be unique among the values of that enum
</pre>

### Edit a Enum's name, description, orderedness ### {#editing-enum}

<pre class=simpledef>
Type: [=Enum=]
Properties: [=name=], [=summary=], [=enumIsOrdered=]
Role: [=maintain=]
Other requirements: [=name=] must be unique among the enums in the [=Workflow=]
</pre>

### Edit a Field's name, description, color ### {#editing-field}

<pre class=simpledef>
Type: [=Field=]
Properties: [=name=], [=summary=], [=fieldColor=]
Role: [=maintain=]
Other requirements:
    [=name=] must be unique among the fields in the [=Workflow=]
</pre>

### Edit a Workflow's name and description ### {#editing-workflow}

<pre class=simpledef>
Type: [=Workflow=]
Properties: [=name=], [=summary=]
Role: [=admin=]
Other requirements: None
</pre>

### Edit a Milestone ### {#s2s-edit-milestone}

<pre class=simpledef>
Type: [=Milestone=]
Properties: [=name=], [=content=], [=source=], [=startTime=], [=endTime=]
Role: [=write=]
Other requirements: None
</pre>

### Edit a Roadmap's name and description ### {#s2s-edit-roadmap}

<pre class=simpledef>
Type: [=Roadmap=]
Properties: [=name=], [=summary=]
Role: [=admin=]
Other requirements: None
</pre>

### Edit a ReleaseTracker's name and description ### {#s2s-edit-release-tracker}

<pre class=simpledef>
Type: [=ReleaseTracker=]
Properties: [=name=], [=summary=]
Role: [=admin=]
Other requirements: None
</pre>

### Edit a Team's name and description ### {#s2s-edit-team}

<pre class=simpledef>
Type: [=Team=]
Properties: [=name=], [=summary=]
Role: [=admin=]
Other requirements: None
</pre>

### Edit a Team's role filter ### {#s2s-edit-team-role-filter}

<pre class=simpledef>
Type: [=Team=]
Properties: [=roleFilter=]
Role: [=admin=]
Other requirements: None
</pre>

### Edit an Organization's name and description ### {#s2s-edit-org}

<pre class=simpledef>
Type: [=Organization=]
Properties: [=name=], [=summary=]
Role: [=admin=]
Other requirements: None
</pre>

## Repository-PatchTracker Links ## {#s2s-repo-pt-link}

### Adding

A link between a [=Repository=] and a [=PatchTracker=] essentially means:

1. The repo officially indicates "send patches via this tracker"
2. The tracker says "I manage patches for this repository"
3. The tracker has access to push commits to the repository (that's how it
    merges Merge Requests into the repo)

Given:

- A [=Repository=] *R*
- A [=PatchTracker=] *P*
- An actor with [=admin=] access to *R*, Luke
- An actor with [=admin=] access to *P*, Aviva

Luke and Aviva can form the link by a direct Grant of the [=write=] role for
*P* to access *R*. See [[#granting-access]]. The process can be initiated via
an [[#access-via-invite|Invite]] or [[#access-via-join|Join]] activity.

Upon successful completion of the process, *R* SHOULD point to *P* via the
[=sendPatchesTo=] property, and *P* SHOULD point to *R* via the
[=tracksPatchesFor=] property.

### Removing

Given:

- A [=Repository=] *R*
- A [=PatchTracker=] *P*
- *R* and *P* have been linked as described above
- An actor with [=admin=] access to *R*, Luke
- An actor with [=admin=] access to *P*, Aviva

Luke or Aviva can remove the link by revoking the [=write=] access, see
[[#revoking-access]]. Once the [=Grant=] is revoked, *R* and *P* MUST stop
pointing to each another via the [=sendPatchesTo=] and [=tracksPatchesFor=]
properties.

If *P* has any open Merge Requests for *R* at the time of revocation, it SHOULD
freeze any further edits of these MRs, making them read-only.

## Workflows ## {#s2s-wf}

Workflows are a powerful customization engine. If you look at a project as an
information system, a database, then workflows allow to extend the data schema
without programming and without launching a patched version of your project
management software.

The project management features that FLOSS-hosting forges provide are usually
very simple and limited. This not not just a practical limitation, but also a
political one: Does the global FLOSS community have sufficient tools for
effective team collaboration?

In ForgeFed, project management software and code hosting software can federate
and provide together a more advanced set of interoperable tools.

In particular, workflows allow to customize the properties of issues and merge
requests. A simple but common use of workflows is to define issue labels (and
often projects "abuse" issue labels to define custom properties, because forges
often don't provide such customization directly).

Workflows also allow to provide forward-compatibility: Implement
new/experimental project tracking features using workflows first, and later
convert to native vocabulary.

Issue: Even me (Pere/fr33domlover), while I'm writing this spec via a Git repo,
I'm tracking my tasks using Vikunja! There's no integration, e.g. Vikunja
tasks linked with PRs on Codeberg. Why is it like this in 2025?

TODO: It's time for the whole (outdated?) concept of email based
task/memo/meeting tracking stuff to come to ActivityPub as well. And implement
federation of that into Nextcloud. Oh well, more dreams than working hands.
Anyone wanna help? :)

### Edit name and description ### {#s2s-wf-edit}

See [[#editing-workflow]].

### Enum ### {#s2s-wf-enum}

#### Create #### {#s2s-wf-enum-create}

Minimal required role: [=maintain=]

Given:

- A [=Workflow=] actor, *W*
- An actor with [=maintain=] access to *W*, Alice

Alice can create a new enumeration type under *W* via the following steps:

1. Alice sends *W* an [=Offer=] activity where:
    - [=object=] is a an object where:
        - [=type=] is [=Enum=]
        - [=name=] is its name
        - [=summary=] is a one-line description
        - [=enumIsOrdered=] is specified
        - [=enumValues=] is an object where:
            - [=type=] is [=OrderedCollection=]
            - [=items=] (or [=orderedItems=]) is a list of objects in which:
                - [=type=] is [=EnumValue=]
                - [=name=] is the value's name
                - [=summary=] is a one-line description
                - [=enumValueColor=] is optionally specified
    - [=target=] is *W*
    - [=capability=] specifies a [=Grant=] activity that gives the relevant
        access
2. *W* inserts the new [=Enum=] into its [=workflowEnums=] collection,
    assigning [=id=] URIs to the [=Enum=] and to each of its values
3. *W* sends an [=Accept=] where:
    - [=object=] refers to the [=Offer=]
    - [=result=] is the [=id=] URI of the new [=Enum=]

#### Delete #### {#s2s-enum-delete}

Minimal required role: [=maintain=]

Given:

- A [=Workflow=] actor, *W*
- An [=Enum=] under *W*, *E*
- An actor with [=maintain=] access to *W*, Alice

Alice can remove *E* from *W* via the following steps:

1. Alice sends *W* a [=Delete=] activity where:
    - [=object=] refers to *E*
    - [=origin=] refers to *W*
    - [=capability=] specifies a [=Grant=] activity that gives the relevant
        access
2. *W* verifies authorization & that none of its [=Field=]s are using *E*
3. *W* removes *E* from its [=workflowEnums=] collection
4. *W* sends an [=Accept=] where [=object=] refers to the [=Delete=]

#### Edit

See [[#editing-enum]].

#### Add Value #### {#s2s-wf-enum-add}

Minimal required role: [=maintain=]

Given:

- A [=Workflow=] actor, *W*
- An [=Enum=] under *W*, *E*
- An actor with [=maintain=] access to *W*, Alice

Alice can add a new value to *E* via the following steps:

1. Alice sends *W* an [=Add=] activity where:
    - [=object=] is a an object where:
        - [=type=] is [=EnumValue=]
        - [=name=] is the value's name
        - [=summary=] is a one-line description
        - [=enumValueColor=] is optionally specified
    - [=target=] is *E*'s [=enumValues=] collection's URI
    - [=capability=] specifies a [=Grant=] activity that gives the relevant
        access
2. *W* inserts the new [=EnumValue=] into *E*'s [=enumValues=] collection,
    assigning an [=id=] URI to the new value
3. *W* sends an [=Accept=] where [=object=] refers to the [=Add=] and
    [=result=] refers to the newly created [=EnumValue=]

#### Remove Value #### {#s2s-wf-enum-remove}

Minimal required role: [=maintain=]

Given:

- A [=Workflow=] actor, *W*
- An [=Enum=] under *W*, *E*
- An [=EnumValue=] within *E*, *V*
- An actor with [=maintain=] access to *W*, Alice

Alice can remove *V* from *E* via the following steps:

1. Alice sends *W* a [=Remove=] activity where:
    - [=object=] refers to *V*
    - [=origin=] refers to *E*'s [=enumValues=] collection
    - [=capability=] specifies a [=Grant=] activity that gives the relevant
        access
2. *W* removes *V* from *E*'s [=enumValues=] collection
3. *W* sends an [=Accept=] where [=object=] refers to the [=Remove=]

#### Reorder Value #### {#s2s-wf-enum-reorder}

Minimal required role: [=maintain=]

Given:

- A [=Workflow=] actor, *W*
- An [=Enum=] under *W*, *E*, whose [=enumIsOrdered=] is `true`
- An [=EnumValue=] within *E*, *V*, in position *p* (e.g. p=3 means *E* is the
    3rd element in the collection of values)
- An actor with [=maintain=] access to *W*, Alice

Alice can move *V* from position *p* to be placed after the element that is
currently at position *q* (and 0 means place at the beginning) via the
following steps:

1. Alice sends *W* a [=Move=] activity where:
    - [=object=] refers to *E*
    - Optionally, for verification:
        - [=originPosition=] is *p*
        - [=target=] refers to the value currently at position *q*
    - [=targetPositionAfter=] is *q*
    - [=capability=] specifies a [=Grant=] activity that gives the relevant
        access
2. *W* moves *E* from position *p* to be placed after the value that is
    currently at position *q*
3. *W* sends an [=Accept=] where [=object=] refers to the [=Move=]

#### Edit a Value

See [[#editing-enum-value]].

### Field ### {#s2s-wf-field}

#### Create #### {#s2s-wf-field-create}

Minimal required role: [=maintain=]

Given:

- A [=Workflow=] actor, *W*
- An actor with [=maintain=] access to *W*, Alice
- If needed, an [=Enum=] within *W*, *E*

Alice can create a new field under *W* via the following steps:

1. Alice sends *W* an [=Offer=] activity where:
    - [=object=] is a an object where:
        - [=type=] is [=Field=]
        - [=name=] is its name
        - [=summary=] is a one-line description
        - [=prop:fieldType=] is specified
        - If [=prop:fieldType=] is [=fieldTypeEnum=]:
            - [=instrument=] refers to *E*
        - [=fieldColor=] is optionally specified
    - [=target=] is *W*
    - [=capability=] specifies a [=Grant=] activity that gives the relevant
        access
2. *W* inserts the new [=Field=] into its [=workflowFields=] collection,
    assigning [=id=] URIs to the [=Field=] and to each of its values
3. *W* sends an [=Accept=] where:
    - [=object=] refers to the [=Offer=]
    - [=result=] is the [=id=] URI of the new [=Field=]

#### Delete #### {#s2s-field-delete}

Minimal required role: [=maintain=]

Given:

- A [=Workflow=] actor, *W*
- An [=Field=] under *W*, *F*
- An actor with [=maintain=] access to *W*, Alice

Alice can remove *F* from *W* via the following steps:

1. Alice sends *W* a [=Delete=] activity where:
    - [=object=] refers to *F*
    - [=origin=] refers to *W*
    - [=capability=] specifies a [=Grant=] activity that gives the relevant
        access
2. *W* removes *F* from its [=workflowFields=] collection
3. *W* sends an [=Accept=] where [=object=] refers to the [=Delete=]

#### Edit

See [[#editing-field]].

#### Reorder #### {#s2s-wf-field-reorder}

Minimal required role: [=maintain=]

Given:

- A [=Workflow=] actor, *W*
- A [=Field=] under *W*, *F*, in position *p* (e.g. p=3 means *F* is the 3rd
    element in the collection of fields)
- An actor with [=maintain=] access to *W*, Alice

Alice can move *F* from position *p* to be placed after the element that is
currently at position *q* (and 0 means place at the beginning) via the
following steps:

1. Alice sends *W* a [=Move=] activity where:
    - [=object=] refers to *F*
    - Optionally, for verification:
        - [=originPosition=] is *p*
        - [=target=] refers to the value currently at position *q*
    - [=targetPositionAfter=] is *q*
    - [=capability=] specifies a [=Grant=] activity that gives the relevant
        access
2. *W* moves *F* from position *p* to be placed after the field that is
    currently at position *q* in *W*'s [=workflowFields=] collection
3. *W* sends an [=Accept=] where [=object=] refers to the [=Move=]

### Tracker-Workflow Link ### {#s2s-wf-link}

### Adding

A link between a [=TicketTracker=]/[=PatchTracker=] and a [=Workflow=]
essentially means:

1. The workflow says "As far as I know, this tracker is using me"
2. The tracker says "I use this workflow in my [=Ticket=]s"

Given:

- A [=Workflow=] *W*
- A [=TicketTracker=] or [=PatchTracker=] *T*
- An actor with [=admin=] access to *W*, Luke
- An actor with [=admin=] access to *T*, Aviva

Luke and Aviva can form the link by a direct Grant of the [=visit=] role for
*T* to access *W*. See [[#granting-access]]. The process can be initiated via
an [[#access-via-invite|Invite]] or [[#access-via-join|Join]] activity.

Upon successful completion of the process:

- *W* SHOULD insert *T* to *W*'s [=workflowTrackers=] collection
- *T* SHOULD point to *W* via [=trackerWorkflow=]
- *T* SHOULD start following *W*, to be able to update its tickets accordingly
    (see [[#following]])

### Removing

Given:

- A [=Workflow=] *W*
- A [=TicketTracker=] or [=PatchTracker=] *T*
- *W* and *T* have been linked as described above
- An actor with [=admin=] access to *W*, Luke
- An actor with [=admin=] access to *T*, Aviva

Luke or Aviva can remove the link by revoking the [=visit=] access, see
[[#revoking-access]]. Once the [=Grant=] is revoked, *W* and *T* MUST stop
pointing to each another via the [=workflowTrackers=] and [=trackerWorkflow=]
properties.

## Roadmaps and Milestones ## {#s2s-roadmap}

Milestones are a way to group issues and MRs and track progress towards goals.
The actor type that manages sets of milestones is [=Roadmap=], and it can work
with one or more tracker.

### Edit roadmap name and description ### {#s2s-roadmap-edit}

See [[#s2s-edit-roadmap]].

### Create milestone ### {#s2s-milestone-create}

Minimal required role: [=write=]

Given:

- A [=Roadmap=] actor, *R*
- An actor with [=write=] access to *R*, Alice

Alice can create a new milestone under *R* via the following steps:

1. Alice sends *R* an [=Offer=] activity where:
    - [=object=] is a an object where:
        - [=type=] is [=Milestone=]
        - [=name=] is its name
        - [=content=] and [=source=] are the description
        - Optionally [=startTime=] and [=endTime=]
    - [=target=] is *R*
    - [=capability=] specifies a [=Grant=] activity that gives the relevant
        access
2. *R* inserts the new [=Milestone=] into its [=roadmapMilestones=] collection,
    assigning it a new [=id=] URI
3. *R* sends an [=Accept=] where:
    - [=object=] refers to the [=Offer=]
    - [=result=] is the [=id=] URI of the new [=Milestone=]

### Delete milestone ### {#s2s-milestone-delete}

Minimal required role: [=write=]

Given:

- A [=Roadmap=] actor, *R*
- A [=Milestone=] under *R*, *m*
- An actor with [=write=] access to *R*, Alice

Alice can remove *m* from *R* via the following steps:

1. Alice sends *R* a [=Delete=] activity where:
    - [=object=] refers to *m*
    - [=origin=] refers to *R*
    - [=capability=] specifies a [=Grant=] activity that gives the relevant
        access
2. *R* removes *m* from its [=roadmapMilestones=] collection
3. *R* sends an [=Accept=] where [=object=] refers to the [=Delete=]

#### Edit milestone

See [[#s2s-edit-milestone]].

### Close milestone ### {#s2s-milestone-close}

Minimal required role: [=write=]

Given:

- A [=Roadmap=] actor, *R*
- An actor with [=write=] access to *R*, Alice
- A [=Milestone=] *m*, under *R*

If *m* is open, i.e. its [=isResolved=] isn't specified or is set to False, *m*
may be closed via the following steps:

1. Alice sends *R* a [=Resolve=] activity where:
    - [=object=] is the URI of *m*
    - [=capability=] is the URI of a [=Grant=] authorizing the action
2. *R* sets *m*'s [=isResolved=] to True, and MAY set [=resolvedBy=] to Alice
    or to the [=Resolve=], and MAY set [=resolved=] to the current time
3. *R* publishes an [=Accept=] whose [=object=] points to the [=Resolve=]

### Reopen ### {#s2s-milestone-reopen}

Minimal required role: [=write=]

Given:

- A [=Roadmap=] actor, *R*
- An actor with [=write=] access to *R*, Alice
- A [=Milestone=] *m*, under *R*

If *m* is closed, i.e. its [=isResolved=] is True, *m* may be reopened via the
following steps:

1. Alice sends *R* a [=Undo=] activity whose:
    - [=object=] is a [=Resolve=] activity whose [=object=] is the URI of *m*
    - [=capability=] is the URI of a [=Grant=] authorizing the action
2. *R* sets *m*'s [=isResolved=] to False
3. *R* publishes an [=Accept=] whose [=object=] points to the [=Undo=]

### Tracker-Roadmap Link ### {#s2s-roadmap-link}

#### Adding

A link between a [=TicketTracker=]/[=PatchTracker=]/[=ReleaseTracker=] and a
[=Roadmap=] essentially means:

1. The Roadmap says "I track this tracker's [=Ticket=]s/[=Release=]s that use
    my milestones"
2. The tracker says "I use this roadmap's milestones in my
    [=Ticket=]s/[=Release=]s"

Given:

- A [=Roadmap=] *R*
- A [=TicketTracker=] or [=PatchTracker=] or [=ReleaseTracker=] *T*
- An actor with [=admin=] access to *R*, Luke
- An actor with [=admin=] access to *T*, Aviva

Luke and Aviva can form the link by a direct Grant of the [=visit=] role for
*T* to access *R*. See [[#granting-access]]. The process can be initiated via
an [[#access-via-invite|Invite]] or [[#access-via-join|Join]] activity.

Upon successful completion of the process:

- *R* SHOULD insert *T* to *R*'s [=roadmapTrackers=] collection
- *T* SHOULD point to *R* via [=trackerRoadmap=]
- *T* SHOULD start following *R*, to be able to update its tickets accordingly
    (see [[#following]])

#### Removing

Given:

- A [=Roadmap=] *R*
- A [=TicketTracker=] or [=PatchTracker=] or [=ReleaseTracker=] *T*
- *R* and *T* have been linked as described above
- An actor with [=admin=] access to *R*, Luke
- An actor with [=admin=] access to *T*, Aviva

Luke or Aviva can remove the link by revoking the [=visit=] access, see
[[#revoking-access]]. Once the [=Grant=] is revoked, *R* and *T* MUST stop
pointing to each another via the [=roadmapTrackers=] and [=trackerRoadmap=]
properties.

## Releases ## {#s2s-release}

### Edit tracker name and description ### {#s2s-release-tracker-edit}

See [[#s2s-edit-release-tracker]].

### Tracker-Repo Link ### {#s2s-release-tracker-repo-link}

#### Adding

A link between a [=Repository=] and a [=ReleaseTracker=] essentially means:

1. The repository says "This tracker tracks my releases"
2. The tracker says "I track releases for this repostory"

Given:

- A [=ReleaseTracker=] *T*
- A [=Repository=] *R*
- An actor with [=admin=] access to *T*, Luke
- An actor with [=admin=] access to *R*, Aviva

Luke and Aviva can form the link by a direct Grant of the [=visit=] role for
*T* to access *R*. See [[#granting-access]]. The process can be initiated via
an [[#access-via-invite|Invite]] or [[#access-via-join|Join]] activity.

Upon successful completion of the process:

- *R* SHOULD point to *T* via [=releasesTrackedBy=]
- *T* SHOULD point to *R* via [=tracksReleasesFor=]
- *T* SHOULD start following *R*, to be able to update releases accordingly
    (see [[#following]])

#### Removing

Given:

- A [=ReleaseTracker=] *T*
- A [=Repository=] *R*
- *T* and *R* have been linked as described above
- An actor with [=admin=] access to *T*, Luke
- An actor with [=admin=] access to *R*, Aviva

Luke or Aviva can remove the link by revoking the [=visit=] access, see
[[#revoking-access]]. Once the [=Grant=] is revoked, *T* and *R* MUST stop
pointing to each another via the [=tracksReleasesFor=] and
[=releasesTrackedBy=] properties.

### Tracker-Roadmap Link ### {#s2s-release-tracker-roadmap-link}

See [[#s2s-roadmap-link]].

### Create release ### {#s2s-release-create}

Minimal required role: [=write=]

Given:

- A [=Repository=] actor, *R*
- A [=ReleaseTracker=] actor, *T*
- *R* and *T* are linked as described in [[#s2s-release-tracker-repo-link]]
- An actor with [=write=] access to *T*, Alice

Alice can create a new release under *T* via the following steps:

1. Alice sends *T* an [=Offer=] activity where:
    - [=object=] is a an object where:
        - [=type=] is [=Release=]
        - [=name=] is the tag to use
        - [=summary=] is the release title
        - [=content=] and [=source=] are the release notes
        - [=origin=] is a [=Commit=] specifying [=context=] (the
            [=Repository=], *R*) and [=hash=]
    - [=target=] is *T*
    - [=capability=] specifies a [=Grant=] activity that gives the relevant
        access
2. *T* inserts the new [=Release=] into its [=releases=] collection, assigning
    it a new [=id=] URI and generating source archive assets
3. *T* sends an [=Accept=] where:
    - [=object=] refers to the [=Offer=]
    - [=result=] is the [=id=] URI of the new [=Release=]

### Delete release ### {#s2s-release-delete}

Minimal required role: [=write=]

Given:

- A [=ReleaseTracker=] actor, *T*
- A [=Release=] under *T*, *r*
- An actor with [=write=] access to *T*, Alice

Alice can remove *r* from *T* via the following steps:

1. Alice sends *T* a [=Delete=] activity where:
    - [=object=] refers to *r*
    - [=origin=] refers to *T*
    - [=capability=] specifies a [=Grant=] activity that gives the relevant
        access
2. *T* removes *r* from its [=releases=] collection
3. *T* sends an [=Accept=] where [=object=] refers to the [=Delete=]

### Associate milestone ### {#s2s-release-milestone-add}

Minimal required role: [=write=]

Given:

- A [=ReleaseTracker=] actor, *T*
- A [=Release=] under *T*, *r*
- A [=Roadmap=] actor *R*
- A [=Milestone=] under *R*, *m*
- An actor with [=write=] access to *T*, Alice

Alice can associate *m* with *r* (i.e. *m* represents work towards the release)
via the following steps:

1. Alice sends an [=Add=] activity where:
    - [=object=] refers to *m*
    - [=target=] refers to *r*'s [=releaseMilestones=] collection
    - [=capability=] specifies a [=Grant=] activity that gives the relevant
        access
2. *T* inserts *m* to *r*'s [=releaseMilestones=] collection
3. *T* sends an [=Accept=] where [=object=] refers to the [=Add=]
4. *R* inserts *r* to *m*'s [=milestoneReleases=] collection

### Unassociate milestone ### {#s2s-release-milestone-remove}

Minimal required role: [=write=]

Given:

- A [=ReleaseTracker=] actor, *T*
- A [=Release=] under *T*, *r*
- A [=Roadmap=] actor *R*
- A [=Milestone=] under *R*, *m*, associated with *r*
- An actor with [=write=] access to *T*, Alice

Alice can remove the association of *m* with *r* via the following steps:

1. Alice sends a [=Remove=] activity where:
    - [=object=] refers to *m*
    - [=origin=] refers to *r*'s [=releaseMilestones=] collection
    - [=capability=] specifies a [=Grant=] activity that gives the relevant
        access
2. *T* removes *m* from *r*'s [=releaseMilestones=] collection
3. *T* sends an [=Accept=] where [=object=] refers to the [=Remove=]
4. *R* removes *r* from *m*'s [=milestoneReleases=] collection

## Organizations ## {#s2s-org}

Organization actors are symbolic entities, whose collections of members and
project aren't tied to the access control system. For example, an
[=Organization=]'s collaborators might be just a few people with [=admin=]
access, but its [=members=] may be a thousand people who belong to the
organization.

### Edit organization name and description ### {#s2s-org-edit}

See [[#s2s-edit-org]].

### Membership ### {#s2s-org-member}

#### Add #### {#s2s-org-member-add}

Minimal required role: [=admin=]

Given:

- An [=Organization=], *O*
- A [=Person=] with [=admin=] access to *O*, Alice
- Another [=Person=], Bob

Alice can declare that Bob is a member of *O* via the following steps:

1. Alice sends an [=Add=] activity where:
    - [=object=] refers to Bob
    - [=target=] refers to *O*'s [=members=] collection
    - [=capability=] refers to a [=Grant=] that authorizes the action
2. *O*, authorizing the [=Add=], sends an [=Accept=] whose [=object=] refers to
    the [=Add=]
3. Bob sends an [=Accept=] whose [=object=] refers to the [=Add=]
4. *O*, seeing Bob's [=Accept=], inserts a new [=Relationship=] object to its
    [=members=] collection, and sends an [=Accept=] again, where:
    - [=object=] refers to the [=Add=]
    - [=result=] refers to the new [=Relationship=] object

#### Remove self #### {#s2s-org-member-remove-self}

Minimal required role: None, just being a member

Given:

- An [=Organization=], *O*
- A [=Person=] who is a member of *O*, Alice

Alice can remove her membership in *O* via the following steps:

1. Alice sends a [=Leave=] activity where:
    - [=object=] refers to *O*'s [=members=] collection
2. *O* removes Alice's [=Relationship=] object from its [=members=] collection
3. *O* sends an [=Accept=] whose [=object=] refers to the [=Leave=]

#### Remove other #### {#s2s-org-member-remove-other}

Minimal required role: [=admin=]

Given:

- An [=Organization=], *O*
- A [=Person=] with [=admin=] access to *O*, Alice
- Another [=Person=], who is a member of *O*, Bob

Alice can remove Bob's membership in *O* via the following steps:

1. Alice sends a [=Remove=] activity where:
    - [=object=] refers to Bob
    - [=origin=] refers to *O*'s [=members=] collection
    - [=capability=] refers to a [=Grant=] that authorizes the action
2. *O* removes Bob's [=Relationship=] object from its [=members=] collection
3. *O* sends an [=Accept=] whose [=object=] refers to the [=Remove=]

### Projects ### {#s2s-org-project}

Minimal required role: [=admin=]

#### Add #### {#s2s-org-project-add}

Assuming:

- An organization *O*
- A project or component *P*
- A person with [=admin=] access to *O*, Ophelia
- A person with [=admin=] access to *P*, Philip

Ophelia can list *P* as a project of *O* via the following steps:

1. Ophelia sends an [=Add=] activity in which:
    - [=object=] refers to *P*
    - [=target=] refers to *O*'s [=orgAssets=] collection
    - [=capability=] is the URI of a [=Grant=] that authorizes Ophelia's admin
        access to *O*
2. *O*, seeing and authorizing the Add, publishes an [=Accept=] where
    [=object=] refers to the Add
3. Philip, seeing the Add and the Accept, sends an [=Accept=] where:
    - [=object=] refers to the Add
    - [=capability=] refers to a [=Grant=] that authorizes Philip's admin
        access to *P*
4. *P*, seeing the Add, seeing *O*'s Accept, and authorizing Philip's Accept,
    publishes an [=Accept=] where [=object=] refers to the Add
5. *O* inserts a new [=Relationship=] object to its [=orgAssets=] collection
6. *O* sends an [=Accept=] where:
    - [=object=] refers to the [=Add=]
    - [=result=] refers to the new [=Relationship=]
7. *P* inserts a [=Relationship=] to its [=organizations=] collection

#### Remove via organization #### {#s2s-org-project-remove-o}

Assuming:

- An organization *O*
- A project or component *P*, listed in *O*'s [=orgAssets=]
- A person with [=admin=] access to *O*, Ophelia

Ophelia can unlist *P* from being a project of *O* via the following steps:

1. Ophelia sends a [=Remove=] activity in which:
    - [=object=] refers to *P*
    - [=origin=] refers to *O*'s [=orgAssets=] collection
    - [=capability=] is the URI of a [=Grant=] that authorizes Ophelia's admin
        access to *O*
2. *O* removes the [=Relationship=] object from its [=orgAssets=] collection
3. *O* sends an [=Accept=] where [=object=] refers to the [=Remove=]
4. *P* removes the [=Relationship=] from its [=organizations=] collection
5. *P* sends an [=Accept=] where [=object=] refers to the [=Remove=]

#### Remove via asset #### {#s2s-org-project-remove-p}

Assuming:

- An organization *O*
- A project or component *P*, listed in *O*'s [=orgAssets=]
- A person with [=admin=] access to *P*, Philip

Philip can unlist *P* from being a project of *O* via the following steps:

1. Philip sends a [=Remove=] activity in which:
    - [=object=] refers to *O*
    - [=origin=] refers to *P*'s [=organizations=] collection
    - [=capability=] is the URI of a [=Grant=] that authorizes Philip's admin
        access to *P*
2. *P* removes the [=Relationship=] from its [=organizations=] collection
3. *P* sends an [=Accept=] where [=object=] refers to the [=Remove=]
4. *O* removes the [=Relationship=] from its [=orgAssets=] collection
5. *O* sends an [=Accept=] where [=object=] refers to the [=Remove=]

<!-- -------------------------------------------------------------------------

#####  ###### #    #   ##   #    # #  ####  #####
#    # #      #    #  #  #  #    # # #    # #    #
#####  #####  ###### #    # #    # # #    # #    #
#    # #      #    # ###### #    # # #    # #####
#    # #      #    # #    #  #  #  # #    # #   #
#####  ###### #    # #    #   ##   #  ####  #    #

-------------------------------------------------------------------------- -->

# Actor Interface and Behavior # {#actor-interface}

This section provides, for each actor types, the various activity types it
receives and how it might react to them in order to implement the features
described in the [[#s2s]] section.

Successful processing SHOULD include inserting the incoming activity to the
receiving actor's inbox & forwarding to the receiving actor's followers if
relevant - for simplicity this step is omitted from some of the behavior
dscriptions below

There are links to the Vervis implementation, since it has additional low-level
detail and notes that might be useful hints to implementors.

## Person ## {#person-iface}

Vervis implementation:
[Vervis.Actor.Person](https://codeberg.org/ForgeFed/Vervis/src/branch/main/src/Vervis/Actor/Person.hs)

Unlike non-human actors, the [=Person=] inbox is also intended for the person
to view incoming notifications. Therefore most activity types, even if
unrelated specifically to the receiving [=Person=], SHOULD still be inserted
into the inbox, for viewing. The interfaces and behaviors described below are
specifically the ones where automated action beyond insertion-to-inbox is
required.

### Accept ### {#person-beh-accept}

Meanings:

- My follow request has been accepted
- An invite, for me to become a collaborator on a resource, has been approved
    by the resource actor
- My request to create an actor via a [=Factory=] has been fulfilled

Processes:

- [[#following]]
- [[#access-via-invite]]
- [[#creating-resource-actors]]

Behavior:

Check the Accept's [=object=] as follows.

- If it's a [=Follow=] whose [=object=] is, or the [=object=]'s managing actor
    is, identical to the Accept's [=actor=]:
    - Insert the Follow [=object=] to my [=following=] collection

- If it's an [=Invite=] where the [=object=] is me:
    - Verify the Accept [=actor=] is the resource (i.e. the actor whose
        [=collaborators=] URI is the Invite's [=target=])
    - Insert to my inbox & forward to my followers

- If it's on a [=Create=] I'd sent to a [=Factory=]:
    - Send a [=Follow=] to the newly created actor (i.e. the Accept's
        [=result=])

### Follow ### {#person-beh-follow}

Meanings:

- Someone is asking to follow me

Processes:

- [[#following]]

Behavior:

- Verify the [=object=] is me
- Verify the [=actor=] isn't already a follower of the [=object=]
- If my account settings require manual approval of follow requests, just wait
    for the human to approve
- Otherwise, i.e. if no manual approval is required:
    - Insert [=actor=] to the list of followers of the [=object=]
    - Send back an [=Accept=] whose [=object=] points to the [=Follow=]

### Grant ### {#person-beh-grant}

Meanings:

- A [=Factory=] is sending me authorization to use it
- I'm becoming a collaborator/member in some resource/team
- A [=Grant=] is being extended to me, due to me being a collaborator in some
    Project or a member of some Team

Processes:

- [[#creating-resource-actors]]

Behavior:

- If it's a [=Factory=] sending me a [=write=] direct-Grant:
    - Insert to my inbox

- If it's a direct-Grant that [=fulfills=] an [=Invite=] or [=Join=] where I'm
    the actor being given access, or a [=Create=] I'd sent to create that
    actor:
    - Verify the [=actor=] is the resource
    - Verify the given role is identical to what was requested in the
        [=Invite=] or [=Join=], or in the case of Create, verify the role is
        [=admin=]
    - If the resource actor is a [=Project=] or [=Team=], send it a
        delegate-Grant, see [[#initial-grant-upon-resource-creation]] step 1,
        or [[#access-via-invite]] step 5, or [[#access-via-join]] option 1 step
        4 or option 2 step 5

- If it's an extension-Grant whose [=capability=] is a delegate-Grant I'd sent:
    - Verify the delegate-Grant is valid and gives [=delegate=] access to the
        extension-Grant's [=actor=]
    - Insert to my inbox

### Reject ### {#person-beh-reject}

Meanings:

- An actor is rejecting my follow request

Processes:

- [[#following]]

Behavior:

- Update the status of the follow request in DB

### Revoke ### {#person-beh-revoke}

Meanings:

- A direct-Grant given to me is being revoked
- An extension-Grant given to me is being revoked

Processes:

- [[#s2s-revoke]]

Behavior:

- Verify the [=actor=] is identical to the actor of the [=Grant=] referred by
    the Revoke's [=object=]
- Verify that Grant's [=target=] is me
- Insert to my inbox & update my tracking of that Grant in DB

### Undo ### {#person-beh-undo}

Meanings:

- Someone is asking to unfollow me

Processes:

- [[#following]]

Behavior:

Check the Undo's [=object=].

- If it's a [=Follow=] whose [=object=] is me:
    - Verify the [=actor=] is a follower of the Follow's [=object=]
    - Remove [=actor=] from the list of followers of the Follow's [=object=]
    - Send back an [=Accept=] whose [=object=] points to the [=Undo=]

## Factory ## {#factory-iface}

Vervis implementation:
[Vervis.Actor.Factory](https://codeberg.org/ForgeFed/Vervis/src/branch/main/src/Vervis/Actor/Factory.hs)

### Accept ### {#factory-beh-accept}

Meanings:

- A collaborator is being added to me
- A Team collaborator is being added to me

Processes:

- [[#access-via-invite]]
- [[#access-via-join]]
- [[#forming-team-resource-link]]

Behavior:

Examine the Accept's [=object=].

- If it's an Invite whose [=target=] is my [=collaborators=] collection:
    - Verify Accept [=actor=] is the Invite's [=target=]
    - Verify this collaborator hasn't been enabled already
    - Enable this collaborator in DB
    - Insert the Accept to my inbox & forward to my followers
    - Send a Grant as described in [[#access-via-invite]] step 3

- If it's a Join whose [=object=] is my [=collaborators=] collection:
    - Verify the Accept is authorized with [=admin=] access
    - Verify this collaborator hasn't been enabled already
    - Enable this collaborator in DB
    - Insert the Accept to my inbox & forward to my followers
    - Send a Grant as described in [[#access-via-join]] step 2

- If it's an Add whose [=target=] is my [=teams=] collection,
    i.e. a Team Collaborator is being added to me and the process is
    initiated on *our* side:
    - Error/ignore, we aren't supposed get any Accept

- If it's an Add whose [=object=] is me and [=target=] points to a collection
    whose [=context=] is a [=Team=] that points back to the collection via
    its [=teamResources=] field, i.e. a Team Collaborator is being added to me
    and the process is initiated on *their* side:
    - Option 1: If I haven't yet seen the Team's Accept:
        Verify the Accept's [=actor=] is the Team
    - Option 2: If I saw Team's Acccept but not yet my collaborator's
        Accept: Verify the Accept is authorized with [=admin=] access
    - Otherwise, error/ignore, no Accept is needed
    - For option 1: Record Team's Accept in DB
    - For option 2: Record my collaborator's Accept in DB
    - Insert the Accept to my inbox & forward to my followers
    - For option 2, send an Accept, see [[#forming-team-resource-link-t]] step
        4

### Add ### {#factory-beh-add}

Meanings:

- A Team collaborator is being added to me

Processes:

- [[#forming-team-resource-link]]

Behavior:

- If the Add [=target=] is my [=teams=] list:
    - Verify the [=object=] is a [=Team=]
    - Verify the Add is authorized with [=admin=] access
    - Verify it's not already an active team of mine
    - Insert the Add to my inbox & forward the Add to my followers
    - Publish an Accept, see [[#forming-team-resource-link-r]] step 2

- If I'm the [=object=], being added to some teams' resource list:
    - Verify the [=target=] is a [=Collection=] whose [=context=] is a [=Team=]
        that points back to the collection via its [=teamResources=] field
    - Verify it's not already an active team of mine
    - Insert the Add to my inbox & forward the Add to my followers

### Create ### {#factory-beh-create}

Meanings:

- I'm being requested to spawn a new resource actor

Processes:

- [[#creating-resource-actors]]

Behavior:

- Verify the Create's [=origin=] is me
- Verify the Create is authorized with [=maintain=] access
- Launch the new actor described by the Create's [=object=]
- Insert the Create to my inbox & forward the Create to my followers
- Publish an Accept, see [[#creating-resource-actors]] step 2

### Delete ### {#factory-beh-delete}

Meanings:

- Someone is asking to delete me

Processes:

- [[#deleting-resource-actors]]

Behavior:

Examine the Delete's [=object=].

- If it's me:
    - Verify the Delete is authorized with [=admin=] access
    - Send an [=Accept=], see [[#deleting-resource-actors]] step 2
    - Delete myself, such that further GET requests for me or any of my
        objects return 404 or 410 status

### Edit ### {#factory-beh-edit}

Meanings:

- My list of available resource actor types is being edited

Processes:

- [[#editing]], in particular [[#editing-factory-types]]

Behavior:

- Verify the [=object=]'s [=id=] is me
- Verify the Edit is authorized with [=admin=] access
- Update my list of available types from [=object=]'s [=availableActorTypes=]
- Insert the Edit to my inbox & forward the Edit to my followers
- Send an Accept, see [[#editing]] step 2

### Follow ### {#factory-beh-follow}

Meanings:

- Someone is asking to follow me

Processes:

- [[#following]]

Behavior:

- Verify the [=object=] is me
- Verify the [=actor=] isn't already a follower of me
- Verify the Follow is authorized with [=visit=] access (unless I'm considered
    a publicly visible resource and then the [=capability=] can be omitted)
- Insert [=actor=] to my list of followers
- Send back an [=Accept=] whose [=object=] points to the [=Follow=]

### Grant ### {#factory-beh-grant}

Meanings:

- A Team-Collaborator-in-process is sending me the delegate-Grant

Processes:

- [[#forming-team-resource-link]]

Behavior:

- Enable the Team Collaborator in DB
- Send the Team a start-Grant, see [[#deleg-step-tr]] step 1

### Invite ### {#factory-beh-invite}

*Same as Invite handler for Repository, Workflow, Roadmap, Project,
Organization.*

Meanings:

- Someone is inviting someone else to be a collaborator in me

Processes:

- [[#access-via-invite]]

Behavior:

- Verify the [=target=] is my [=collaborators=] collection
- Verify the Invite is authorized with [=admin=] access
- Verify the [=object=] isn't already a collaborator of me
- Insert the Invite to my inbox & forward to followers
- Publish an [=Accept=] whose [=object=] points to the Invite

Note: The Accept in the last step does NOT indicate that a collaborator has
been added. It merely signals to the potential collaborator that the Invite is
authorized, and now they can decide whether to accept/reject/ignore it.

### Join ### {#factory-beh-join}

*Same as Join handler for the Component actors (TicketTracker, PatchTracker,
Repository, Workflow, Roadmap, ReleaseTracker) and for Project, Organization.*

Meanings:

- Someone is asking to become a collaborator in me

Processes:

- [[#access-via-join]]

Behavior:

- Verify the [=object=] is my [=collaborators=] collection
- Verify the [=actor=] isn't already a collaborator of me
- If the Join specifies a [=capability=]:
    - Verify the Join is authorized with [=admin=] access
    - Insert the Join to my inbox & forward to my followers
    - Publish a [=Grant=], see [[#access-via-join]] option 1 step 2.2
- Otherwise, i.e. if it doesn't:
    - Insert the Join to my inbox & forward to my followers

### Remove ### {#factory-beh-remove}

Meanings:

- One of my Collaborators is being removed
- One of my Team Collaborators is being removed

Processes:

- [[#taking-away-access-using-remove-activities]]
- [[#removing-team-resource-link]]

Behavior:

- If [=origin=] is my [=collaborators=] collection:
    - Verify the Remove is authorized with [=admin=] access
    - Verify the [=object=] is one of my collaborators
    - Remove the [=object=] actor from my list of collaborators and invalidate
        the direct-Grant I had sent them
    - Insert the Remove to my inbox & forward to my followers
    - Publish a [=Revoke=], see [[#taking-away-access-using-remove-activities]]
        step 2.4

- If [=origin=] is my [=teams=] collection:
    - Verify the Remove is authorized with [=admin=] access
    - Verify the [=object=] is one of my Team Collaborators
    - Remove the [=object=] actor from my list of Team Collaborators and
        invalidate the start-Grant I had sent it
    - Insert the Remove to my inbox & forward to my followers
    - Publish an [=Accept=], see [[#removing-team-resource-link-r]] step 2

- If I'm the [=object=], being removed from the [=origin=] specifying the
    [=teamResources=] collection of a Team Collaborator of mine:
        - Nothing to do, just waiting for the Team to send a Revoke on the
            delegate-Grant it had sent me

### Revoke ### {#factory-beh-revoke}

Meanings:

- A Team Collaborator of mine is revoking the delegate-Grant it gave me

Processes:

- [[#removing-team-resource-link]]

Behavior:

- Verify the [=actor=] is a Team Collaborator of mine
- Verify the [=object=] is the delegate-Grant it had sent me
- Remove the Team from my [=teams=] collection and invalidate the start-Grant I
    had sent it
- Insert the Revoke to my inbox & forward to my followers
- Publish an Accept, see [[#removing-team-resource-link-t]] step 3

### Undo ### {#factory-beh-undo}

Meanings:

- Someone is asking to unfollow me

Processes:

- [[#following]]

Behavior:

- Verify the [=object=] is a Follow whose [=object=] is me
- Verify the [=actor=] is a follower of me
- Remove [=actor=] from my list of followers
- Send back an [=Accept=] whose [=object=] points to the [=Undo=]

## Team ## {#team-iface}

Vervis implementation:
[Vervis.Actor.Group](https://codeberg.org/ForgeFed/Vervis/src/branch/main/src/Vervis/Actor/Group.hs)

### Accept ### {#team-beh-accept}

Meanings:

- Someone is becoming a Collaborator in me
- I'm gaining access to a resource
- I'm becoming a child/parent of another Team
- I'm becoming an overseen/oversight of another Team
- My child/parent link with another team is being removed
- My overseen/oversight link with another team is being removed
- I'm being um/assigned to/from a ticket/MR

Processes:

- [[#access-via-invite]]
- [[#access-via-join]]
- [[#forming-team-resource-link]]
- [[#linking-parent-child]]
- [[#linking-oversight]]
- [[#issue-assign]]

Behavior:

Check the Accept's [=object=] as follows.

- If it's an Invite whose [=target=] is my [=collaborators=] collection:
    - Verify Accept [=actor=] is the Invite's [=target=]
    - Verify this collaborator hasn't been enabled already
    - Enable this collaborator in DB
    - Insert the Accept to my inbox & forward to my followers
    - Send a Grant as described in [[#access-via-invite]] step 3

- If it's a Join whose [=object=] is my [=collaborators=] collection:
    - Verify the Accept is authorized with [=admin=] access
    - Verify this collaborator hasn't been enabled already
    - Enable this collaborator in DB
    - Insert the Accept to my inbox & forward to my followers
    - Send a Grant as described in [[#access-via-join]] step 2

- If it's an Add whose [=target=] is my [=context=] (i.e. parents) collection,
    and [=object=] is a [=Team=], i.e. I'm getting a new parent team and the
    process was initiated on *our* side:
    - Verify I haven't seen the parent team'ss Accept yet
    - If Accept [=actor=] is the parent team:
        - Verify I've already published my own Accept
        - Publish a delegate-Grant, see [[#linking-parent-child-t-c]] step
            5
    - Otherwise:
        - Just forward to my followers (hopefully this is the parent team's
            collaborator giving their approval)

- If it's an Add whose [=object=] is me and whose [=target=] is the URI of the
    [=subteams=] of some [=Team=], i.e. I'm getting a new parent team and
    the process was initiated on *their* side:
    - If I haven't seen the parent team's Accept yet:
        - Verify the Accept [=actor=] is the parent
        - Insert to my inbox & forward to my followers
    - If I've already seen the parent team's Accept, but not yet my
        collaborator's Accept:
        - Verify the (new) Accept is authorized with [=admin=] access
        - Send a delegate-Grant, see [[#linking-parent-child-t-p]] step 4

- If it's an Add whose [=target=] is my [=subteams=] collection, and [=object=]
    is some [=Team=], i.e. I'm getting a new child and the process was
    initiated on *our* side:
    - Ignore/error, I'm not expecting any Accept

- If it's an Add whose [=object=] is me and whose [=target=] is the
    [=context=] (i.e. parents) collection of some [=Team=], i.e. I'm getting a
    new child and the process was initiated on *their* side:
    - If I haven't yet seen the child's Accept:
        - Verify the Accept [=actor=] is the child
        - Insert to my inbox & forward to my followers
    - If I've seen it, but haven't yet seen my collaborator's Accept:
        - Verify the Accept is authorized with [=admin=] access
        - Send an Accept, see [[#linking-parent-child-t-c]] step 4

- If it's a Remove whose [=object=] is me and whose [=origin=] is the URI of
    the [=subteams=] of some [=Team=], i.e. a parent team of mine is being
    removed and the process was initiated on *their* side:
    - Verify the [=actor=] is the parent
    - Publish a [=Revoke=], see [[#unlinking-parent-child-t-p]] step 3
    - Remove the parent from my [=context=] collection
    - Publish a [=Revoke=] on every extension-Grant I've published as an
        extension of a Grant I received from the parent, see
        [[#unlinking-parent-child-t-p]] after step 4

- If it's an Add whose [=target=] is my [=teamResources=] collection and
    [=object=] is some resource actor, i.e. I'm becoming a Team Collaborator in
    a resource and the process was initiated on *our* side:
    - Verify I haven't yet seen the resource's Accept
    - If sender is the resource:
        - Send a delegate-Grant, see [[#forming-team-resource-link-t]] step 5
    - Otherwise:
        - Just forward to my followers

- If it's an Add whose [=object=] is me and whose [=target=] is some resource
    actor's [=teams=] collection, i.e. I'm becoming a Team Collaborator in a
    resource and the process was initiated on *their* side:
    - If I haven't seen the resource actor's Accept yet:
        - Verify [=actor=] is the resource
        - Insert to my inbox & forward to my followers
    - If I've seen it, but not yet my collaborator's Accept:
        - Verify the (new) Accept is authorized with [=admin=] access
        - Send a delegate-Grant, see [[#forming-team-resource-link-r]] step 4

- If it's a Remove whose [=object=] is me and whose [=origin=] is the URI of
    the [=teams=] of some resource actor, i.e. my Team Collaborator access to
    some resource is being revoked and the process was initiated on *their*
    side:
    - Verify the [=actor=] is the resource
    - Publish a [=Revoke=], see [[#removing-team-resource-link-r]] step 3
    - Remove the resource from my [=teamResources=] collection
    - Publish a [=Revoke=] on every extension-Grant I've published as an
        extension of a Grant I received from the resource, see
        [[#removing-team-resource-link-r]] after step 3

- If it's an [=Assign=] whose [=object=] is me and [=target=] is some tracker's
    ticket/MR:
    - If I've already seen the tracker's Accept that specifies a [=result=],
        ignore/error, otherwise proceed
    - Option 1: If the Accept [=actor=] is the tracker, and [=result=] isn't specified,
        and I've haven't yet seen the tracker's Accept:
        - Insert to my inbox & forward to my followers
    - Option 2: f the Accept has a [=capability=] and I haven't yet seen my
        collaborator's Accept:
        - Verify the [=Accept=] is authorized with [=triage=] access
        - Insert to my inbox & forward to my followers
    - If I've now seen both of those Accepts, publish my own Accept, see
        [[#issue-assign-other]] step 4
    - Option 3: If I already saw both Accepts before:
        - Verify Accept [=actor=] is the tracker
        - Verify [=result=] is specified
        - Insert to my inbox & forward to my followers

- If it's an [=Undo=] whose [=object=] is an [=Assign=] where [=object=] is me
    and [=target=] is some tracker's ticket/MR:
    - Verify the Accept's [=actor=] is the tracker
    - If it doesn't specify a [=result=] and I haven't yet seen the tracker's
        Accepts:
        - Publish an Accept, see [[#issue-unassign-other]] step 3
        - Insert to my inbox & forward to my followers
    - If it specifies a [=result=] and I haven't yet seen the tracker's
        Accept-with-[=result=]:
        - Just insert to my inbox & forward to my followers

- If it's an Add whose [=target=] is my [=oversees=] collection,
    and [=object=] is a [=Team=], i.e. I'm getting a new overseen team and the
    process was initiated on *our* side:
    - Verify I haven't seen the other team's Accept yet
    - If Accept [=actor=] is the other team:
        - Verify I've already published my own Accept
        - Publish a delegate-Grant, see [[#linking-oversight-c]] step 5
    - Otherwise:
        - Just forward to my followers (hopefully this is the other team's
            collaborator giving their approval)

- If it's an Add whose [=object=] is me and whose [=target=] is the URI of the
    [=overseenBy=] of some [=Team=], i.e. I'm getting a new overseen team and
    the process was initiated on *their* side:
    - If I haven't seen the other team's Accept yet:
        - Verify the Accept [=actor=] is the other team
        - Insert to my inbox & forward to my followers
    - If I've already seen the other team's Accept, but not yet my
        collaborator's Accept:
        - Verify the (new) Accept is authorized with [=admin=] access
        - Send a delegate-Grant, see [[#linking-oversight-p]] step 4

- If it's an Add whose [=target=] is my [=overseenBy=] collection, and
    [=object=] is some [=Team=], i.e. I'm getting a new oversight and the
    process was initiated on *our* side:
    - Ignore/error, I'm not expecting any Accept

- If it's an Add whose [=object=] is me and whose [=target=] is the
    [=oversees=] collection of some [=Team=], i.e. I'm getting a new oversight
    and the process was initiated on *their* side:
    - If I haven't yet seen the other team's Accept:
        - Verify the Accept [=actor=] is the other team
        - Insert to my inbox & forward to my followers
    - If I've seen it, but haven't yet seen my collaborator's Accept:
        - Verify the Accept is authorized with [=admin=] access
        - Send an Accept, see [[#linking-oversight-c]] step 4

- If it's a Remove whose [=object=] is me and whose [=origin=] is the URI of
    the [=overseenBy=] of some [=Team=], i.e. an overseen team of mine is being
    removed and the process was initiated on *their* side:
    - Verify the [=actor=] is the other team
    - Publish a [=Revoke=], see [[#unlinking-oversight-p]] step 3
    - Remove the team from my [=oversees=] collection
    - Publish a [=Revoke=] on every extension-Grant I've published as an
        extension of the start-Grant I received from the team, see
        [[#unlinking-oversight-p]] after step 4

### Add ### {#team-beh-add}

Meanings:

- I'm gaining access to a resource
- A child/parent/oversight team is being added to me
- Someone is asking to list their MR review as being in my name

Processes:

- [[#forming-team-resource-link]]
- [[#linking-parent-child]]
- [[#linking-oversight]]
- [[#s2s-mr-review-team-list]]

Behavior:

- If [=target=] is my [=subteams=] collection, i.e. I'm getting a new
    child and the process is initiated on *our* side:
    - Verify [=object=] is a [=Team=]
    - Verify the Add is authorized with [=admin=] access
    - Verify it's not already a child or parent of mine
    - Publish an Accept, see [[#linking-parent-child-t-p]] step 2

- If [=target=] is my [=context=] (i.e. parents) collection, i.e. I'm getting a
    new parent and the process is initiated on *our* side:
    - Verify [=object=] is a [=Team=]
    - Verify the Add is authorized with [=admin=] access
    - Verify it's not already a child or parent of mine
    - Publish an Accept, see [[#linking-parent-child-t-c]] step 2

- If [=target=] is my [=teamResources=] collection, i.e. I'm becoming a Team
    Collaborator of some resource and the process is initiated on *our* side:
    - Verify the [=object=] a resource actor that isn't a Team
    - Verify the Add is authorized with [=admin=] access
    - Verify I don't already have access to this resource
    - Publish an Accept, see [[#forming-team-resource-link-t]] step 2

- If [=object=] is me and [=target=] is some [=Team=]'s [=context=] (i.e.
    parents) collection, i.e. I'm getting a new child and the process is
    initiated on *their* side:
    - Verify I'm not already a child or parent of mine
    - Insert to my inbox & forward to my followers

- If [=object=] is me and [=target=] is some [=Team=]'s [=subteams=]
    collection, i.e. I'm getting a new parent and the process is initiated on
    *their* side:
    - Verify I'm not already a child or parent of mine
    - Insert to my inbox & forward to my followers

- If [=object=] is me and [=target=] is some resource actor's [=teams=]
    collection, i.e. I'm becoming a Team Collaborator of some resource and the
    process is initiated on *their* side:
    - Verify I'm not already a Team Collaborator of this resource
    - Insert to my inbox & forward to my followers

- If [=object=] refers to an [=Approval=] and [=target=] refers to another
    [=Approval=]:
    - Verify the action is authorized with [=write=] access
    - Verify the [=object=] approval is attributed to an individual
    - Verify the [=target=] approval is attributed to me
    - Send an [=Accept=], see [[#s2s-mr-review-team-list]] step 2

- If [=target=] is my [=overseenBy=] collection, i.e. I'm getting a new
    oversight and the process is initiated on *our* side:
    - Verify [=object=] is a [=Team=]
    - Verify the Add is authorized with [=admin=] access
    - Verify it's not a parent of mine
    - Publish an Accept, see [[#linking-oversight-p]] step 2

- If [=target=] is my [=oversees=] collection, i.e. I'm getting a new overseen
    and the process is initiated on *our* side:
    - Verify [=object=] is a [=Team=]
    - Verify the Add is authorized with [=admin=] access
    - Verify it's not a child of mine
    - Publish an Accept, see [[#linking-oversight-c]] step 2

- If [=object=] is me and [=target=] is some [=Team=]'s [=oversees=]
    collection, i.e. I'm getting a new oversight and the process is
    initiated on *their* side:
    - Verify it's not a parent of mine
    - Insert to my inbox & forward to my followers

- If [=object=] is me and [=target=] is some [=Team=]'s [=overseenBy=]
    collection, i.e. I'm getting a new overseen and the process is initiated on
    *their* side:
    - Verify it's not a child of mine
    - Insert to my inbox & forward to my followers

### Assign ### {#team-beh-assign}

Meanings:

- I'm being assigned to a ticket/MR

Processes:

- [[#issue-assign]]

Behavior:

- Verify the [=object=] is me
- Verify the [=target=] is a ticket/MR to which I'm not already assigned
- Insert to my inbox & forward to my followers

### Delete ### {#team-beh-delete}

Meanings:

- Someone is asking to delete me

Processes:

- [[#deleting-resource-actors]]

Behavior:

- If [=object=] is me:
    - Verify the Delete is authorized with [=admin=] access
    - Send an [=Accept=], see [[#deleting-resource-actors]] step 2
    - Delete myself, such that further GET requests for me or any of my
        objects return 404 or 410 status

### Edit ### {#team-beh-edit}

Meanings:

- Someone is asking to edit my name and/or description
- Someone is asking to edit my role filter

Processes:

- [[#s2s-edit-team]]
- [[#s2s-edit-team-role-filter]]

Behavior:

Check the Edit's [=object=]'s [=id=].

- If it's me:
    - Verify the Edit is authorized with [=admin=] access
    - Update me in DB from the Edit [=object=]'s fields:
        - Either [=name=] and [=summary=],
        - Or [=roleFilter=]

### Follow ### {#team-beh-follow}

Meanings:

- Someone is asking to follow me

Processes:

- [[#following]]

Behavior:

- Verify the [=object=] is me
- Verify the [=actor=] isn't already a follower of the [=object=]
- Verify the Follow is authorized with [=visit=] access (unless I'm considered
    a publicly visible resource and then the [=capability=] can be omitted)
- Insert [=actor=] to the list of followers of the [=object=]
- Send back an [=Accept=] whose [=object=] points to the [=Follow=]

### Grant ### {#team-beh-grant}

Meanings:

- Resource sending me a start-Grant or extension-Grant
- Member sending me the delegate-Grant
- Parent team sending me an extension-Grant
- Almost-child team sending me the delegate-Grant
- Overseen team sending me the start-Grant
- Almost-oversight team sending me the delegate-Grant

Processes:

- [[#access-via-invite]]
- [[#access-via-join]]
- [[#linking-parent-child]]
- [[#linking-oversight]]
- [[#forming-team-resource-link]]

Behavior:

- Resource sending me a start-Grant or extension-Grant:
    - Verify the Grant is authorized by the delegate-Grant I had sent to the
        resource
    - Send extension-Grants to each team member and child team of mine, see
        [[#extending-a-delegation-chain]]

- Member sending me the delegate-Grant:
    - Verify the Grant is authorized by the direct-Grant I had sent to the
        member
    - Verify I don't yet have a delegate-Grant from this member
    - For each start-Grant or extension-Grant I received from a parent or
        resource, send an extension-Grant to the member, see
        [[#extending-a-delegation-chain]]

- Parent team sending me an extension-Grant:
    - Verify the Grant is authorized by the delegate-Grant I had sent to the
        parent
    - Send an extension-Grant to every team member and child team, see
        [[#extending-a-delegation-chain]]

- Almost-child team sending me the delegate-Grant:
    - Send extension-Grants, see [[#deleg-step-t]]

- Overseen team sending me the start-Grant:
    - Verify the Grant is authorized by the delegate-Grant I had sent to that
        team
    - Send an extension-Grant to every team member and child team, see
        [[#extending-a-delegation-chain]]

- Almost-oversight team sending me the delegate-Grant:
    - Send extension-Grants, see [[#deleg-step-o]]

### Invite ### {#team-beh-invite}

Same as for [=Factory=], see [[#factory-beh-invite]].

### Join ### {#team-beh-join}

Same as for [=Factory=], see [[#factory-beh-join]].

### Reject ### {#team-beh-reject}

Same as for [=TicketTracker=], see [[#tt-beh-reject]].

### Remove ### {#team-beh-remove}

Meanings:

- One of my members is being removed
- One of my resources is being removed
- One of my parents or children is being removed
- One of my oversights or overseen is being removed

Processes:

- [[#taking-away-access-using-remove-activities]]
- [[#removing-team-resource-link]]
- [[#linking-parent-child]]
- [[#linking-oversight]]

Behavior:

- If [=origin=] is my [=collaborators=] collection:
    - Verify the Remove is authorized with [=admin=] access
    - Verify the [=object=] is one of my members
    - Remove the [=object=] actor from my list of members and invalidate
        the direct-Grant I had sent them
    - Publish a [=Revoke=], see [[#taking-away-access-using-remove-activities]]
        step 2.4
    - Publish [=Revoke=] activities on all the extension-Grants I've sent to
        this collaborator

- If [=origin=] is my [=context=] (i.e. parents) collection:
    - Verify the Remove is authorized with [=admin=] access
    - Verify the [=object=] is one of my parents
    - Remove the [=object=] actor from my [=context=] collection
    - Publish an [=Revoke=] on the delegate-Grant, see
        [[#unlinking-parent-child-t-c]] step 2
    - Publish [=Revoke=]s on all extension-Grants I've sent, that extend a
        start-Grant or extension-Grant I'd received from the parent

- If [=origin=] is my [=subteams=] collection:
    - Verify the Remove is authorized with [=admin=] access
    - Verify the [=object=] is one of my children
    - Remove the [=object=] actor from my [=subteams=] collection
    - Send an [=Accept=], see [[#unlinking-parent-child-t-p]] step 2

- If [=origin=] is my [=teamResources=] collection:
    - Verify the Remove is authorized with [=admin=] access
    - Verify the [=object=] is in my [=teamResources=] collection
    - Remove the [=object=] actor from my [=teamResources=] collection
    - Publish an [=Revoke=] on the delegate-Grant, see
        [[#removing-team-resource-link-t]] step 2
    - Publish [=Revoke=]s on all extension-Grants I've sent, that extend a
        start-Grant or extension-Grant I'd received from the resource

- If I'm the [=object=], being removed from the [=subteams=] collection of a
    parent of mine:
    - Just forward to my followers

- If I'm the [=object=], being removed from the [=context=] (i.e. parents)
    collection of a child of mine:
    - Do nothing, just waiting for child to send a Revoke on the
        delegate-Grant

- If I'm the [=object=], being removed from the [=teams=] collection of one of
    my resources:
    - Just forward to my followers

- If [=origin=] is my [=oversees=] collection:
    - Verify the Remove is authorized with [=admin=] access
    - Verify the [=object=] is one of my overseen teams
    - Remove the [=object=] actor from my [=oversees=] collection
    - Publish an [=Revoke=] on the delegate-Grant, see
        [[#unlinking-oversight-c]] step 2
    - Publish [=Revoke=]s on all extension-Grants I've sent, that extend the
        start-Grant I'd received from the team

- If [=origin=] is my [=overseenBy=] collection:
    - Verify the Remove is authorized with [=admin=] access
    - Verify the [=object=] is one of my oversight teams
    - Remove the [=object=] actor from my [=overseenBy=] collection
    - Send an [=Accept=], see [[#unlinking-oversight-p]] step 2

- If I'm the [=object=], being removed from the [=overseenBy=] collection of an
    overseen team of mine:
    - Just forward to my followers

- If I'm the [=object=], being removed from the [=oversees=] collection of a
    oversight team of mine:
    - Do nothing, just waiting for the team to send a Revoke on the
        delegate-Grant

### Resolve ### {#team-beh-resolve}

Meanings:

- Someone wants to send a team approval on a MR

Processes:

- [[#s2s-mr-review-team-approve]]

Behavior:

- If [=object=] refers to an [=Approval=]:
    - Verify the action is authorized with [=write=] access
    - Send an [=Offer=] activity, see [[#s2s-mr-review-team-approve]] step 2

### Undo ### {#team-beh-undo}

Meanings:

- Someone is asking to unfollow me
- I'm being unassigned from a ticket/MR

Processes:

- [[#following]]
- [[#issue-assign]]

Behavior:

Check the Undo's [=object=].

- If it's a [=Follow=] whose [=object=] is me:
    - Verify the [=actor=] is a follower of the Follow's [=object=]
    - Remove [=actor=] from the list of followers of the Follow's [=object=]
    - Send back an [=Accept=] whose [=object=] points to the [=Undo=]

- If it's an [=Assign=] whose [=object=] is me:
    - Verify the Assign's [=target=] is some tracker's ticket/MR
    - Verify I'm currently assigned to that ticket/MR
    - Insert to my inbox & forward to my followers

## Project ## {#project-beh}

Vervis implementation:
[Vervis.Actor.Project](https://codeberg.org/ForgeFed/Vervis/src/branch/main/src/Vervis/Actor/Project.hs)

### Accept ### {#project-beh-accept}

Meanings:

- Someone is becoming a Collaborator in me
- A component is being added to me
- A Team is becoming a Team Collaborator in me
- I'm becoming a child/parent of another Project
- My child/parent link with another project is being removed
- I'm being added/removed to/from an organization

Processes:

- [[#access-via-invite]]
- [[#access-via-join]]
- [[#linking-projects-components]]
- [[#forming-team-resource-link]]
- [[#linking-parent-child]]
- [[#s2s-org-project]]

Behavior:

Check the Accept's [=object=] as follows.

- If it's an Invite whose [=target=] is my [=collaborators=] collection:
    - Verify Accept [=actor=] is the Invite's [=target=]
    - Verify this collaborator hasn't been enabled already
    - Enable this collaborator in DB
    - Insert the Accept to my inbox & forward to my followers
    - Send a Grant as described in [[#access-via-invite]] step 3

- If it's a Join whose [=object=] is my [=collaborators=] collection:
    - Verify the Accept is authorized with [=admin=] access
    - Verify this collaborator hasn't been enabled already
    - Enable this collaborator in DB
    - Insert the Accept to my inbox & forward to my followers
    - Send a Grant as described in [[#access-via-join]] step 2

- If it's an [=Add=] where [=target=] is my [=components=] collection and
    [=object=] is a component actor, i.e. a component is being added to me and
    the process was inisiated on our side:
    - If the Accept [=actor=] is the component:
        - Insert the component to my [=components=] collection
        - Send a delegate-Grant, see [[#linking-projects-components-p]] step 5
    - If it's not the component:
        - Just forward the Accept to my followers (hopefully it is the
            component's collaborator giving their approval)

- If it's an [=Add=] where I'm the [=object=] and [=target=] is the URI of a
    collection whose [=context=] is a component that points back to the
    collection via its own [=context=] field, i.e. a component is being added
    to me and the process was initiated on the component's side:
    - If Accept [=actor=] is the component:
        - Verify I haven't seen the component's Accept yet
        - Insert to my inbox & forward to my followers
    - If it isn't the component:
        - Verify I've seen the component's Accept
        - Verify the (new) Accept is authorized with [=admin=] access
        - Insert the component to my [=components=] collection
        - Send a delegate-Grant, see [[#linking-projects-components-c]] step 4

- If it's an Add whose [=target=] is my [=subprojects=] collection, and
    [=object=] is a [=Project=], i.e. I'm getting a new child project and the
    process was initiated on *our* side:
    - Verify I haven't seen the child project's Accept yet
    - If Accept [=actor=] is the child project:
        - Verify I've already published my own Accept
        - Publish a delegate-Grant, see [[#linking-parent-child-p-p]] step
            4
    - Otherwise:
        - Just forward to my followers (hopefully this is the child
            project's collaborator giving their approval)

- If it's an Add whose [=object=] is me and whose [=target=] is the URI of the
    [=context=] of some [=Project=], i.e. I'm getting a new child project and
    the process was initiated on *their* side:
    - If I haven't seen the child project's Accept yet:
        - Verify the Accept [=actor=] is the child
        - Insert to my inbox & forward to my followers
    - If I've already seen the child project's Accept, but not yet my
        collaborator's Accept:
        - Verify the (new) Accept is authorized with [=admin=] access
        - Send a delegate-Grant, see [[#linking-parent-child-p-c]] step 4

- If it's an Add whose [=target=] is my [=context=] (i.e. parents) collection,
    and [=object=] is some [=Project=], i.e. I'm getting a new parent and the
    process was initiated on *our* side:
    - Ignore/error, I'm not expecting any Accept

- If it's an Add whose [=object=] is me and whose [=target=] is the
    [=subprojects=] collection of some [=Project=], i.e. I'm getting a new
    parent and the process was initiated on *their* side:
    - If I haven't yet seen the parent's Accept:
        - Verify the Accept [=actor=] is the parent
        - Insert to my inbox & forward to my followers
    - If I've seen it, but haven't yet seen my collaborator's Accept:
        - Verify the Accept is authorized with [=admin=] access
        - Send an Accept, see [[#linking-parent-child-p-p]] step 4

- If it's a Remove whose [=object=] is me and whose [=origin=] is the URI of the
    [=context=] of some [=Project=], i.e. a child project of mine is being
    removed and the process was initiated on *their* side:
    - Verify the [=actor=] is the child
    - Publish a [=Revoke=], see [[#unlinking-parent-child-p-c]] step 3
    - Publish a [=Revoke=] on every extension-Grant I've published as an
        extension of a Grant I received from the child, see
        [[#unlinking-parent-child-p-c]] step 5

- If it's an [=Add=] whose [=object=] is me and whose [=target=] is some
    [=Organization=]'s [=orgAssets=] collection:
    - Verify I haven't yet seen the organization's Accept-with-result
    - If [=actor=] is the organization and [=result=] is specified:
        - Insert a [=Relationship=] object to my [=organizations=] collection
    - If I haven't yet seen my collaborator's accept, and [=capability=] is
        specified:
        - Verify the Accept is authorized with [=admin=] access
        - Send an [=Accept=], see [[#s2s-org-project-add]] step 4

- If it's a [=Remove=] where [=object=] is me and [=origin=] is some
    [=Organization=]'s [=orgAssets=] collection:
    - Verify I haven't seen the organization's Accept yet
    - Verify [=actor=] is the organization
    - Remove the organization from my [=organizations=] collection
    - Send an [=Accept=], see [[#s2s-org-project-remove-o]] step 5

### Add ### {#project-beh-add}

Meanings:

- A Team collaborator is being added to me
- A component is being added to me
- A child/parent project is being added to me
- I'm being added to an organization

Processes:

- [[#forming-team-resource-link]]
- [[#linking-projects-components]]
- [[#linking-parent-child]]
- [[#s2s-org-project-add]]

Behavior:

- If [=target=] is my [=components=] collection, i.e. I'm getting a new
    component and the process is initiated on *our* side:
    - Verify the Add is authorized with [=admin=] access
    - Verify the [=object=] is a component actor
    - Verify I don't yet have this actor as a component
    - Publish an Accept, see [[#linking-projects-components-p]] step 2

- If [=target=] is my [=subprojects=] collection, i.e. I'm getting a new
    child and the process is initiated on *our* side:
    - Verify [=object=] is a [=Project=]
    - Verify the Add is authorized with [=admin=] access
    - Verify it's not already a child or parent of mine
    - Publish an Accept, see [[#linking-parent-child-p-p]] step 2

- If [=target=] is my [=context=] (i.e. parents) collection, i.e. I'm getting a
    new parent and the process is initiated on *our* side:
    - Verify [=object=] is a [=Project=]
    - Verify the Add is authorized with [=admin=] access
    - Verify it's not already a child or parent of mine
    - Publish an Accept, see [[#linking-parent-child-p-c]] step 2

- If [=target=] is my [=teams=] collection, i.e. a Team Collaborator is being
    added to me and the process is initiated on *our* side:
    - Verify the [=object=] is a [=Team=]
    - Verify the Add is authorized with [=admin=] access
    - Verify it's not already an active team of mine
    - Publish an Accept, see [[#forming-team-resource-link-r]] step 2


- If [=object=] is me and [=target=] is some [=Project=]'s [=context=] (i.e.
    parents) collection, i.e. I'm getting a new child and the process is
    initiated on *their* side:
    - Verify I'm not already a child or parent of mine
    - Insert to my inbox & forward to my followers

- If [=object=] is me and [=target=] is some [=Project=]'s [=subprojects=]
    collection, i.e. I'm getting a new parent and the process is initiated on
    *their* side:
    - Verify I'm not already a child or parent of mine
    - Insert to my inbox & forward to my followers

- If [=object=] is me and [=target=] is some [=Team=]'s [=teamResources=]
    collection, i.e. a Team Collaborator is being added to me and the process
    is initiated on *their* side:
    - Verify it's not already an active team of mine
    - Insert to my inbox & forward to my followers

- If [=object=] is me and [=target=] is some component actor's [=context=]
    (i.e. projects) collection, i.e. a component is being added to me and the
    process is initiated on *their* side:
    - Verify it's not already a component of mine
    - Insert to my inbox & forward to my followers

- If [=object=] is me and [=target=] is some [=Organization=]'s [=orgAssets=]
    collection:
    - Just waiting for the organization's Accept and my collaborator's Accept

### Delete ### {#project-beh-delete}

Meanings:

- Someone is asking to delete me

Processes:

- [[#deleting-resource-actors]]

Behavior:

- If [=object=] is me:
    - Verify the Delete is authorized with [=admin=] access
    - Send an [=Accept=], see [[#deleting-resource-actors]] step 2
    - Delete myself, such that further GET requests for me or any of my
        objects return 404 or 410 status

### Edit ### {#project-beh-edit}

TODO editing project name and description

### Follow ### {#project-beh-follow}

Meanings:

- Someone is asking to follow me

Processes:

- [[#following]]

Behavior:

- Verify the [=object=] is me
- Verify the [=actor=] isn't already a follower of the [=object=]
- Verify the Follow is authorized with [=visit=] access (unless I'm considered
    a publicly visible resource and then the [=capability=] can be omitted)
- Insert [=actor=] to the list of followers of the [=object=]
- Send back an [=Accept=] whose [=object=] points to the [=Follow=]

### Grant ### {#project-beh-grant}

Meanings:

- Component sending me the start-Grant
- Collaborator sending me the delegate-Grant
- Child project sending me a start-Grant or extension-Grant
- Almost-parent project sending me the delegate-Grant
- Almost-team sending me the delegate-Grant

Processes:

- [[#linking-projects-components]]
- [[#access-via-invite]]
- [[#access-via-join]]
- [[#linking-parent-child]]
- [[#forming-team-resource-link]]

Behavior:

- Component sending me a start-Grant:
    - Verify role is same one specified in the [=Add=]
    - Verify the Grant is authorized by the delegate-Grant I had sent to the
        component
    - Send extension-Grants to each collaborator, team collaborator and parent
        project of mine, see [[#extending-a-delegation-chain]]

- Collaborator sending me a delegate-Grant:
    - Verify the Grant is authorized by the direct-Grant I had sent to the
        collaborator
    - Verify I don't yet have a delegate-Grant from this collaborator
    - Sending extension-Grants to the collaborator, see
        [[#extending-a-delegation-chain]]:
        - Each start-Grant I received from a component of mine
        - Each start-Grant or extension-Grant I received from a child of mine

- Child sending me a start-Grant or extension-Grant:
    - Verify it's authorized with the delegate-Grant I had sent to the child
    - Send extension-Grants to each collaborator, team collaborator and parent
        project of mine, see [[#extending-a-delegation-chain]]

- Almost-Parent sending me the delegate-Grant:
    - Send a start-Grant and extension-Grants, see [[#deleg-step-j]]

- Almost-Team-Collaborator sending me the delegate-Grant:
    - Send a start-Grant and extension-Grants, see [[#deleg-step-j]]

### Invite ### {#project-beh-invite}

Same as for [=Factory=], see [[#factory-beh-invite]].

### Join ### {#project-beh-join}

Same as for [=Factory=], see [[#factory-beh-join]].

### Like ### {#project-beh-like}

Meanings:

- Someone wants to star me

Processes:

- [[#s2s-star]]

Behavior:

- Verify the [=Like=] is authorized with [=visit=] access (unless I'm
    considered a public resource and then the [=capability=] can be omitted)
- Verify [=actor=] is a [=Person=]
- Verify the [=actor=] hasn't already Liked me
- Insert the [=Like=] to my [=likes=] collection
- If [=capability=] was specified:
    - Send back an [=Accept=], see [[#s2s-star-add]] step 4

### Reject ### {#project-beh-reject}

Same as for [=TicketTracker=], see [[#tt-beh-reject]].

### Remove ### {#project-beh-remove}

Meanings:

- One of my Collaborators is being removed
- One of my Team Collaborators is being removed
- One of my components is being removed
- One of my parents or children is being removed
- One of my organizations is being removed

Processes:

- [[#taking-away-access-using-remove-activities]]
- [[#removing-team-resource-link]]
- [[#linking-projects-components]]
- [[#linking-parent-child]]
- [[#s2s-org-project]]

Behavior:

- If [=origin=] is my [=collaborators=] collection:
    - Verify the Remove is authorized with [=admin=] access
    - Verify the [=object=] is one of my collaborators
    - Remove the [=object=] actor from my list of collaborators and invalidate
        the direct-Grant I had sent them
    - Publish a [=Revoke=], see [[#taking-away-access-using-remove-activities]]
        step 2.4
    - Publish [=Revoke=] activities on all the extension-Grants I've sent to
        this collaborator

- If [=origin=] is my [=components=] collection:
    - Verify the Remove is authorized with [=admin=] access
    - Verify the [=object=] is one of my components
    - Remove the [=object=] actor from my components collection
    - Publish an [=Revoke=] on the delegate-Grant, see
        [[#unlinking-projects-components-p]] step 2
    - Publish [=Revoke=]s on all extension-Grants I've sent, that extend the
        start-Grant I'd received from the component

- If [=origin=] is my [=subprojects=] collection:
    - Verify the Remove is authorized with [=admin=] access
    - Verify the [=object=] is one of my children
    - Remove the [=object=] actor from my [=subprojects=] collection
    - Publish an [=Revoke=] on the delegate-Grant, see
        [[#unlinking-parent-child-p-p]] step 2
    - Publish [=Revoke=]s on all extension-Grants I've sent, that extend a
        start-Grant or extension-Grant I'd received from the child

- If [=origin=] is my [=context=] (i.e. parents) collection:
    - Verify the Remove is authorized with [=admin=] access
    - Verify the [=object=] is one of my parents
    - Remove the [=object=] actor from my [=context=] collection
    - Send an [=Accept=], see [[#unlinking-parent-child-p-c]] step 2

- If [=origin=] is my [=teams=] collection:
    - Verify the Remove is authorized with [=admin=] access
    - Verify the [=object=] is one of my Team Collaborators
    - Remove the [=object=] actor from my list of Team Collaborators and
        invalidate the start-Grant I had sent it
    - Publish an [=Accept=], see [[#removing-team-resource-link-r]] step 2

- If I'm the [=object=], being removed from the [=context=] (i.e. parents)
    collection of a child of mine:
    - Just forward to my followers

- If I'm the [=object=], being removed from the [=subprojects=] collection of a
    parent of mine:
    - Do nothing, just waiting for parent to send a Revoke on the
        delegate-Grant

- If I'm the [=object=], being removed from the [=context=] (i.e. projects)
    collection of a component of mine:
    - Do nothing, just waiting for component to send a Revoke on the
        start-Grant

- If I'm the [=object=], being removed from the [=teamResources=] collection of
    a Team Collaborator of mine:
    - Nothing to do, just waiting for the Team to send a Revoke on the
        delegate-Grant it had sent me

- If [=object=] is me and [=origin=] is some [=Organization=]'s [=orgAssets=]
    collection:
    - Just waiting for the organization's Accept

- If [=origin=] is my [=organizations=] collection and [=object=] is some
    [=Organization=]:
    - Verify the Remove is authorized with [=admin=] access
    - Remove the organization from my [=organizations=] collection
    - Send an [=Accept=], see [[#s2s-org-project-remove-p]] step 3

### Undo ### {#project-beh-undo}

Meanings:

- Someone is asking to unfollow me
- Someone wants to unstar me

Processes:

- [[#following]]
- [[#s2s-star]]

Behavior:

Check the Undo's [=object=].

- If it's a [=Follow=] whose [=object=] is me:
    - Verify the [=actor=] is a follower of the Follow's [=object=]
    - Remove [=actor=] from the list of followers of the Follow's [=object=]
    - Send back an [=Accept=] whose [=object=] points to the [=Undo=]

- If it's a [=Like=] whose [=object=] is me:
    - Verify the [=Like=] is authorized with [=visit=] access (unless I'm
        considered a public resource and then the [=capability=] can be omitted)
    - Verify the [=actor=] has Liked me
    - Remove the [=actor=]'s past [=Like=] from my [=likes=] collection
    - If [=capability=] was specified:
        - Send back an [=Accept=], see [[#s2s-star-remove]] step 4

## Repository ## {#repo-iface}

Vervis implementation:
[Vervis.Actor.Repo](https://codeberg.org/ForgeFed/Vervis/src/branch/main/src/Vervis/Actor/Repo.hs)

### Accept ### {#repo-beh-accept}

Meanings:

- Someone is becoming a Collaborator in me
- A Team is becoming a Team Collaborator in me
- I'm becoming a component of a Project

Processes:

- [[#access-via-invite]]
- [[#access-via-join]]
- [[#forming-team-resource-link]]
- [[#linking-projects-components]]

Behavior:

Check the Accept's [=object=] as follows.

- If it's an Invite whose [=target=] is my [=collaborators=] collection:
    - Verify Accept [=actor=] is the Invite's [=target=]
    - Verify this collaborator hasn't been enabled already
    - Enable this collaborator in DB
    - If [=instrument=] is [=visit=] and [=object=] is a [=ReleaseTracker=]:
        - Verify my [=releasesTrackedBy=] isn't set
        - Point to the [=object=] via [=releasesTrackedBy=]
    - Send a Grant as described in [[#access-via-invite]] step 3

- If it's a Join whose [=object=] is my [=collaborators=] collection:
    - Verify the Accept is authorized with [=admin=] access
    - Verify this collaborator hasn't been enabled already
    - Enable this collaborator in DB
    - If [=instrument=] is [=visit=] and [=actor=] is a [=ReleaseTracker=]:
        - Verify my [=releasesTrackedBy=] isn't set
        - Point to the [=object=] via [=releasesTrackedBy=]
    - Send a Grant as described in [[#access-via-join]] step 2

- If it's an Add whose [=target=] is my [=teams=] collection,
    i.e. a Team Collaborator is being added to me and the process is
    initiated on *our* side:
    - Error/ignore, we aren't supposed get any Accept

- If it's an Add whose [=object=] is me and [=target=] points to a collection
    whose [=context=] is a [=Team=] that points back to the collection via
    its [=teamResources=] field, i.e. a Team Collaborator is being added to me
    and the process is initiated on *their* side:
    - Option 1: If I haven't yet seen the Team's Accept:
        Verify the Accept's [=actor=] is the Team
    - Option 2: If I saw Team's Acccept but not yet my collaborator's
        Accept: Verify the Accept is authorized with [=admin=] access
    - Otherwise, error/ignore, no Accept is needed
    - For option 1: Record Team's Accept in DB
    - For option 2: Record my collaborator's Accept in DB
    - Insert the Accept to my inbox & forward to my followers
    - For option 2, send an Accept, see [[#forming-team-resource-link-t]]
        step 4

- If it's an Add whose [=target=] is my [=context=] (i.e. projects) collection,
    i.e. I'm being added to a project and the process is initiated on *our*
    side:
    - Nothing to do, just ignore the Accept, waiting for the delegate-Grant
        from the Project

- If it's an Add whose [=object=] is me and [=target=] points to a collection
    whose [=context=] is a [=Project=] that points back to the collection via
    its [=components=] field, i.e. I'm being added to a project and the process
    is initiated on *their* side:
    - If I haven't yet seen the project's approval:
        - Verify the author is the project
        - Insert to my inbox & forward to my followers
    - If I saw project's approval, but not my collaborators' approval:
        - Verify the Accept is authorized with [=admin=] access
        - Insert to my inbox & forward to my followers
        - Publish an Accept, see [[#linking-projects-components-p]] step 4
    - If I already saw both approvals, respond with error

### Add ### {#repo-beh-add}

Same as for [=TicketTracker=], see [[#tt-beh-add]].

### Delete ### {#repo-beh-delete}

Meanings:

- Someone is asking to delete me

Processes:

- [[#deleting-resource-actors]]

Behavior:

- If [=object=] is me:
    - Verify the Delete is authorized with [=admin=] access
    - Send an [=Accept=], see [[#deleting-resource-actors]] step 2
    - Delete myself, such that further GET requests for me or any of my
        objects return 404 or 410 status

### Edit ### {#repo-beh-edit}

TODO edit default branch?

### Follow ### {#repo-beh-follow}

Meanings:

- Someone is asking to follow me

Processes:

- [[#following]]

Behavior:

- Verify the [=object=] is me
- Verify the [=actor=] isn't already a follower of the [=object=]
- Verify the Follow is authorized with [=visit=] access (unless I'm considered
    a publicly visible resource and then the [=capability=] can be omitted)
- Insert [=actor=] to the list of followers of the [=object=]
- Send back an [=Accept=] whose [=object=] points to the [=Follow=]

### Grant ### {#repo-beh-grant}

Meanings:

- A Project-in-process is sending me the delegate-Grant
- A Team-Collaborator-in-process is sending me the delegate-Grant

Processes:

- [[#linking-projects-components]]
- [[#forming-team-resource-link]]

Behavior:

Check the Grant's [=fulfills=] field, verify it's an [=Add=] activity that I've
seen:

- If it's a Project-in-process which is now sending me the delegate-Grant, and
    the previous steps in the process successfully happened:
    - Insert the Project to my list of Projects and enable in DB
    - Send the Project a start-Grant, see [[#linking-projects-components-c]]
        step 5 or [[#linking-projects-components-p]] step 6

- If it's a Team-Collaborator-in-process which is now sending me the
    delegate-Grant, and the previous steps in the process successfully
    happened:
    - Insert the Team to my list of Team Collaborators and enable in DB
    - Send the Team a start-Grant, see [[#deleg-step-tr]] step 1

### Invite ### {#repo-beh-invite}

Same as for [=Factory=], see [[#factory-beh-invite]].

### Join ### {#repo-beh-join}

Same as for [=Factory=], see [[#factory-beh-join]].

### Like ### {#repo-beh-like}

Meanings:

- Someone wants to star me

Processes:

- [[#s2s-star]]

Behavior:

- Verify the [=Like=] is authorized with [=visit=] access (unless I'm
    considered a public resource and then the [=capability=] can be omitted)
- Verify [=actor=] is a [=Person=]
- Verify the [=actor=] hasn't already Liked me
- Insert the [=Like=] to my [=likes=] collection
- If [=capability=] was specified:
    - Send back an [=Accept=], see [[#s2s-star-add]] step 4

### Reject ### {#repo-beh-reject}

Same as for [=TicketTracker=], see [[#tt-beh-reject]].

### Remove ### {#repo-beh-remove}

Meanings:

- One of my Collaborators is being removed
- One of my Team Collaborators is being removed
- I'm being removed from one of my Projects

Processes:

- [[#taking-away-access-using-remove-activities]]
- [[#removing-team-resource-link]]
- [[#linking-projects-components]]

Behavior:

- If [=origin=] is my [=collaborators=] collection:
    - Verify the Remove is authorized with [=admin=] access
    - Verify the [=object=] is one of my collaborators
    - Remove the [=object=] actor from my list of collaborators and invalidate
        the direct-Grant I had sent them
    - If [=object=] is my [=releasesTrackedBy=], unset my [=releasesTrackedBy=]
        field
    - Publish a [=Revoke=], see [[#taking-away-access-using-remove-activities]]
        step 2.4

- If [=origin=] is my [=context=] (i.e. projects) collection:
    - Verify the Remove is authorized with [=admin=] access
    - Verify the [=object=] is one of the projects in which I'm a component
    - Remove the [=object=] actor from my list of projects and invalidate the
        start-Grant I had sent it
    - Publish an [=Revoke=], see [[#unlinking-projects-components-c]] step 2

- If [=origin=] is my [=teams=] collection:
    - Verify the Remove is authorized with [=admin=] access
    - Verify the [=object=] is one of my Team Collaborators
    - Remove the [=object=] actor from my list of Team Collaborators and
        invalidate the start-Grant I had sent it
    - Publish an [=Accept=], see [[#removing-team-resource-link-r]] step 2

- If I'm the [=object=], being removed from the [=origin=] specifying the
    [=components=] collection of a Project of mine:
        - Just forward to my followers

- If I'm the [=object=], being removed from the [=origin=] specifying the
    [=teamResources=] collection of a Team Collaborator of mine:
        - Nothing to do, just waiting for the Team to send a Revoke on the
            delegate-Grant it had sent me

### Revoke ### {#tt-beh-revoke}

Meanings:

- A direct-Grant given to me by the [=Workflow=] is being revoked

Processes:

- [[#revoking-access]]

Behavior:

- Verify the [=actor=] is identical to the actor of the [=Grant=] referred by
    the Revoke's [=object=]
- Verify that Grant's [=target=] is me
- Optional: Verify none of my tickets use custom fields OR remove all custom
    fields
- Remove my [=trackerWorkflow=] field

### Undo ### {#repo-beh-undo}

Meanings:

- Someone is asking to unfollow me
- Someone wants to unstar me

Processes:

- [[#following]]
- [[#s2s-star]]

Behavior:

Check the Undo's [=object=].

- If it's a [=Follow=] whose [=object=] is me:
    - Verify the [=actor=] is a follower of the Follow's [=object=]
    - Remove [=actor=] from the list of followers of the Follow's [=object=]
    - Send back an [=Accept=] whose [=object=] points to the [=Undo=]

- If it's a [=Like=] whose [=object=] is me:
    - Verify the [=Like=] is authorized with [=visit=] access (unless I'm
        considered a public resource and then the [=capability=] can be omitted)
    - Verify the [=actor=] has Liked me
    - Remove the [=actor=]'s past [=Like=] from my [=likes=] collection
    - If [=capability=] was specified:
        - Send back an [=Accept=], see [[#s2s-star-remove]] step 4

## TicketTracker ## {#tt-iface}

Vervis implementation:
[Vervis.Actor.Deck](https://codeberg.org/ForgeFed/Vervis/src/branch/main/src/Vervis/Actor/Deck.hs)

### Accept ### {#tt-beh-accept}

Meanings:

- Someone is becoming a Collaborator in me
- A Team is becoming a Team Collaborator in me
- I'm becoming a component of a Project
- Someone is being assigned to one of my tickets
- I'm becoming a collaborator in a [=Workflow=]
- A dependency on one of my tickets is being added or removed
- A milestone is being deleted

Processes:

- [[#access-via-invite]]
- [[#access-via-join]]
- [[#forming-team-resource-link]]
- [[#linking-projects-components]]
- [[#issue-assign]]
- [[#s2s-wf-link]]
- [[#s2s-dep]]
- [[#s2s-milestone-delete]]

Behavior:

Check the Accept's [=object=] as follows.

- If it's an Invite whose [=target=] is my [=collaborators=] collection:
    - Verify Accept [=actor=] is the Invite's [=target=]
    - Verify this collaborator hasn't been enabled already
    - Enable this collaborator in DB
    - Insert the Accept to my inbox & forward to my followers
    - Send a Grant as described in [[#access-via-invite]] step 3

- If it's a Join whose [=object=] is my [=collaborators=] collection:
    - Verify the Accept is authorized with [=admin=] access
    - Verify this collaborator hasn't been enabled already
    - Enable this collaborator in DB
    - Insert the Accept to my inbox & forward to my followers
    - Send a Grant as described in [[#access-via-join]] step 2

- If it's an Add whose [=target=] is my [=teams=] collection,
    i.e. a Team Collaborator is being added to me and the process is
    initiated on *our* side:
    - Error/ignore, we aren't supposed get any Accept

- If it's an Add whose [=object=] is me and [=target=] points to a collection
    whose [=context=] is a [=Team=] that points back to the collection via
    its [=teamResources=] field, i.e. a Team Collaborator is being added to me
    and the process is initiated on *their* side:
    - Option 1: If I haven't yet seen the Team's Accept:
        Verify the Accept's [=actor=] is the Team
    - Option 2: If I saw Team's Acccept but not yet my collaborator's
        Accept: Verify the Accept is authorized with [=admin=] access
    - Otherwise, error/ignore, no Accept is needed
    - For option 1: Record Team's Accept in DB
    - For option 2: Record my collaborator's Accept in DB
    - Insert the Accept to my inbox & forward to my followers
    - For option 2, send an Accept, see [[#forming-team-resource-link-t]]
        step 4

- If it's an Add whose [=target=] is my [=context=] (i.e. projects) collection,
    i.e. I'm being added to a project and the process is initiated on *our*
    side:
    - Nothing to do, just ignore the Accept, waiting for the delegate-Grant
        from the Project

- If it's an Add whose [=object=] is me and [=target=] points to a collection
    whose [=context=] is a [=Project=] that points back to the collection via
    its [=components=] field, i.e. I'm being added to a project and the process
    is initiated on *their* side:
    - If I haven't yet seen the project's approval:
        - Verify the author is the project
        - Insert to my inbox & forward to my followers
    - If I saw project's approval, but not my collaborators' approval:
        - Verify the Accept is authorized with [=admin=] access
        - Insert to my inbox & forward to my followers
        - Publish an Accept, see [[#linking-projects-components-p]] step 4
    - If I already saw both approvals, respond with error

- If it's an [=Assign=] I've seen and authorized, where [=target=] is one of my
    tickets, and where [=actor=] and [=object=] aren't the same actor:
    - Verify the Assign's [=object=] actor isn't already assigned to this
        ticket
    - If the Accept's [=actor=] is the Assign's [=object=]:
        - Insert an item into *t*'s [=assignments=] collection, see
            [[#issue-assign-other]] step 5
        - Send an [=Accept=], see [[#issue-assign-other]] step 6
    - Otherwise:
        - If Assign's [=object=] is a [=Team=]:
            - Just forward to my followers (hopefully this is one of the Team's
                collaborators giving their approval)
        - Otherwise:
            - Ignore/error, not expecting such an Accept

- If it's an Undo I've seen and authorized, whose [=object=] is an [=Assign=]
    where [=target=] is one of my tickets, and where [=actor=] and [=object=]
    aren't the same actor:
    - Verify the Assign's [=object=] is currently assigned to the ticket
    - Verify the Accept's [=actor=] is the Assign's [=object=]
    - Update the ticket's [=assignments=] collection, see
        [[#issue-unassign-other]] step 4
    - Send an Accept, see [[#issue-unassign-other]] step 5

- If it's an Invite I've seen, whose [=object=] is me, and whose [=target=] is
    some [=Workflow=]'s [=collaborators=] collection, and whose [=instrument=]
    is [=visit=]:
    - Verify Accept [=actor=] is the [=Workflow=]
    - Verify I don't have a [=trackerWorkflow=]
    - Send an [=Accept=], see [[#access-via-invite]] step 2

- If it's an [=Offer=] I've seen, of a [=TicketDependency=] where one of my
    tickets is the child:
    - Verify the [=actor=] is the parent ticket's tracker
    - Insert the [=TicketDependency=], referred by the Accept's [=result=], to
        the child's [=dependants=] collection

- If it's an [=Delete=] I've seen, of a [=TicketDependency=] where one of my
    tickets is the child:
    - Verify the [=actor=] is the parent ticket's tracker
    - Remove the [=TicketDependency=] from the child's [=dependants=]
        collection

- If it's a [=Delete=] I've seen, of a [=Milestone=] that belongs to my
    [=trackerRoadmap=]:
    - Update my tickets, unsetting their milestone if they belong to the
        milestone being deleted

### Add ### {#tt-beh-add}

*Same as Accept handler for Repository.*

Meanings:

- A Team collaborator is being added to me
- I'm being added as a component of a Project

Processes:

- [[#forming-team-resource-link]]
- [[#linking-projects-components]]

Behavior:

- If [=target=] is my [=teams=] collection, i.e. a Team Collaborator is being
    added to me and the process is initiated on *our* side:
    - Verify the [=object=] is a [=Team=]
    - Verify the Add is authorized with [=admin=] access
    - Verify it's not already an active team of mine
    - Insert the Add to my inbox & forward the Add to my followers
    - Publish an Accept, see [[#forming-team-resource-link-r]] step 2

- If [=object=] is me and [=target=] points to a collection
    whose [=context=] is a [=Team=] that points back to the collection via
    its [=teamResources=] field, i.e. a Team Collaborator is being added to me
    and the process is initiated on *their* side:
    - Verify it's not already an active team of mine
    - Insert to my inbox & forward to my followers

- If [=target=] is my [=context=] (i.e. projects) collection, i.e. I'm being
    added to a project and the process is initiated on *our* side:
    - Verify the Add is authorized with [=admin=] access
    - Verify the [=object=] is a [=Project=]
    - Verify I'm not yet a component of this Project
    - Insert to my inbox & forward to my followers
    - Publish an Accept, see [[#linking-projects-components-c]] step 2

- If [=object=] is me and [=target=] points to a collection whose [=context=]
    is a [=Project=] that points back to the collection via its [=components=]
    field, i.e. I'm being added to a project and the process is initiated on
    *their* side:
    - Verify I'm not already a component of this Project
    - Insert to my inbox & forward to my followers

### Assign ### {#tt-beh-assign}

*Same as Assign handler for PatchTracker.*

Meanings:

- A [=Person=] or [=Team=] is being assigned to one of my tickets

Processes:

- [[#issue-assign]]

Behavior:

- Verify [=target=] is one of my tickets
- Verify the Assign is authorized with [=triage=] access
- If the [=actor=] is a [=Person=] assigning themselves:
    - Insert a [=Relationship=] object to the ticket's [=assignments=]
        collection, see [[#issue-assign-self]] step 2
    - Send an [=Accept=], see [[#issue-assign-self]] step 3
- If [=actor=] and [=object=] aren't the same actor:
    - Send an [=Accept=], see [[#issue-assign-other]] step 2

### Create ### {#tt-beh-create}

Meanings:

- Someone is commenting on a Ticket of mine

Processes:

- [[#commenting]]

Behavior:

- Verify [=object=] is a [=Note=] whose [=context=] is a [=Ticket=] of mine
- Insert to inbox & forward to my followers & ticket followers

### Delete ### {#tt-beh-delete}

Meanings:

- Someone is deleting their comment on a ticket of mine
- Someone is asking to delete a ticket of mine
- Someone is asking to delete me
- Someone is removing a ticket dependency
- A milestone is being deleted

Processes:

- [[#commenting]]
- [[#deleting-issue]]
- [[#deleting-resource-actors]]
- [[#s2s-dep-remove]]
- [[#s2s-milestone-delete]]

Behavior:

- If [=object=] is a [=Note=] whose [=context=] is a [=Ticket=] of mine:
    - Verify sender is the actual author of the Note
    - Insert to inbox & forward to my followers & ticket followers

- If [=object=] is a ticket of mine:
    - Verify the Delete is authorized with [=admin=] access
    - Delete the ticket from DB, such that GET requests for it now return 404
        or 410 status
    - Send an [=Accept=], see [[#deleting-issue]] step 3

- If [=object=] is me:
    - Verify the Delete is authorized with [=admin=] access
    - Send an [=Accept=], see [[#deleting-resource-actors]] step 2
    - Delete myself, such that further GET requests for me or any of my
        objects return 404 or 410 status

- If [=origin=] is me and [=object=] is a [=TicketDependency=]:
    - Verify the Delete is authorized with [=triage=] access
    - Verify it's a [=TicketDependency=] I'm hosting, whose [=subject=] is one
        of my tickets
    - Remove the [=TicketDependency=] from the [=subject=]'s [=dependencies=]
        collection
    - Send an [=Accept=], see [[#s2s-dep-remove]] step 3
    - If the [=TicketDependency=]'s [=object=] is one of my tickets:
        - Remove the [=TicketDependency=] from the [=object=]'s [=dependants=]
            collection

- If [=origin=] isn't me and [=object=] is a [=TicketDependency=]:
    - Verify the [=TicketDependency=]'s [=object=] is one of my tickets
    - Verify the [=TicketDependency=] is listed in the ticket's [=dependants=]
        collection
    - Just insert to my inbox & forward to my followers

- If [=origin=] is my [=trackerRoadmap=] and [=object=] is a [=Milestone=]:
    - Just insert to my inbox & forward to my followers and to the followers of
        every ticket of mine that uses this milestone

### Edit ### {#tt-beh-edit}

Meanings:

- Someone is deleting their comment on a ticket of mine
- Someone is asking to edit the text of one of my tickets
- Someone is asking to edit the custom fields of one of my tickets
- Someone is asking to edit the milestone of one of my tickets

Processes:

- [[#commenting]]
- [[#editing-ticket]]
- [[#editing-ticket-custom]]
- [[#s2s-edit-ticket-milestone]]

Behavior:

- If [=object=] is a [=Note=] whose [=context=] is a [=Ticket=] of mine:
    - Verify sender is the actual author of the Note
    - Insert to inbox & forward to my followers & ticket followers

- If [=object=] is a [=Ticket=] whose [=context=] is me:
    - If [=object=] has [=summary=] and/or [=content=]:
        - Verify the Edit is authorized with [=triage=] access, OR the [=actor=]
            is the same actor as the one who submitted the ticket
        - Update the ticket in DB from the Edit [=object=]'s fields:
            [=summary=], [=source=], [=content=]
    - If [=object=] specifies only [=customFields=] AND I have a
        [=trackerWorkflow=] set:
        - Verify the Edit is authorized with [=triage=] access
        - For each object listed under [=customFields=]:
            - Verify it has a [=context=] that refers to a [=Field=]
            - Verify it has a [=prop:fieldValue=] specifying a matching value
            - If the value is `null`:
                - Update DB, removing the custom field from the ticket
            - Otherwise:
                - Update DB, updating the custom field on the ticket
    - If [=object=] has only [=ticketMilestone=]:
        - Verify the Edit is authorized with [=triage=] access
        - Verify I have a [=trackerRoadmap=] set
        - Verify the ticket's new milestone, if set, belongs to that tracker
        - Update the ticket's milestone in DB

### Follow ### {#tt-beh-follow}

Meanings:

- Someone is asking to follow me or a Ticket of mine

Processes:

- [[#following]]

Behavior:

- Verify the [=object=] is me or one of my tickets
- Verify the [=actor=] isn't already a follower of the [=object=]
- Verify the Follow is authorized with [=visit=] access (unless I'm considered
    a publicly visible resource and then the [=capability=] can be omitted)
- Insert [=actor=] to the list of followers of the [=object=]
- Send back an [=Accept=] whose [=object=] points to the [=Follow=]

### Grant ### {#tt-beh-grant}

*Same as Grant handler for PatchTracker.*

Meanings:

- A Project-in-process is sending me the delegate-Grant
- A Team-Collaborator-in-process is sending me the delegate-Grant
- A [=Workflow=] is sending me the direct-Grant

Processes:

- [[#linking-projects-components]]
- [[#forming-team-resource-link]]
- [[#s2s-wf-link]]

Behavior:

Check the Grant's [=fulfills=] field, verify it's an
[=Add=]/[=Invite=]/[=Join=] activity that I've seen:

- If it's a Project-in-process which is now sending me the delegate-Grant, and
    the previous steps in the process successfully happened:
    - Insert the Project to my list of Projects and enable in DB
    - Send the Project a start-Grant, see [[#linking-projects-components-c]]
        step 5 or [[#linking-projects-components-p]] step 6

- If it's a Team-Collaborator-in-process which is now sending me the
    delegate-Grant, and the previous steps in the process successfully
    happened:
    - Insert the Team to my list of Team Collaborators and enable in DB
    - Send the Team a start-Grant, see [[#deleg-step-tr]] step 1

- If it's a [=Workflow=], in which I'm becoming a [=visit=] Collaborator, now
    sending me the direct-Grant:
    - Verify I don't have a [=trackerWorkflow=] yet
    - Set [=trackerWorkflow=] to refer to this [=Workflow=]

### Invite ### {#tt-beh-invite}

Meanings:

- Someone is inviting someone else to be a collaborator in me
- Someone is inviting me to be a collaborator in a [=Workflow=]

Processes:

- [[#access-via-invite]]
- [[#s2s-wf-link]]

Behavior:

- If [=target=] is my [=collaborators=] collection:
    - Verify the Invite is authorized with [=admin=] access
    - Verify the [=object=] isn't already a collaborator of me
    - Publish an [=Accept=] whose [=object=] points to the Invite

Note: The Accept in the last step does NOT indicate that a collaborator has
been added. It merely signals to the potential collaborator that the Invite is
authorized, and now they can decide whether to accept/reject/ignore it.

- If [=object=] is me and [=target=] is some [=Workflow=]'s [=collaborators=]
    collection:
    - Verify [=instrument=] is [=visit=]
    - Just insert to my inbox & forward to my followers

### Join ### {#tt-beh-join}

Same as for [=Factory=], see [[#factory-beh-join]].

### Offer ### {#tt-beh-offer}

Meanings:

- Someone is submitting a new ticket
- Someone is submitting a ticket dependency

Processes:

- [[#opening-issue]]
- [[#s2s-dep-add]]

Behavior:

- If [=target=] is me and [=object=] is a [=Ticket=]:
    - Create new Ticket in DB
    - Publish an [=Accept=], see [[#opening-issue]]

- If [=target=] is me and [=object=] is a [=TicketDependency=]:
    - Verify the Offer is authorized with [=triage=] access
    - Verify the [=TicketDependency=]'s [=subject=] is one of my tickets
    - Verify the [=TicketDependency=]'s [=relationship=] is [=dependsOn=]
    - Insert the [=TicketDependency=] to the [=subject=]'s [=dependencies=]
        collection
    - Send an [=Accept=], see [[#s2s-dep-add]] step 3
    - If the [=TicketDependency=]'s [=object=] is one of my tickets:
        - Insert the [=TicketDependency=] to the [=object=]'s [=dependants=]
            collection

- If [=target=] isn't me and [=object=] is a [=TicketDependency=]:
    - Verify the [=TicketDependency=]'s [=object=] is one of my tickets
    - Verify the [=TicketDependency=]'s [=relationship=] is [=dependsOn=]
    - Just insert to my inbox & forward to my followers

### Reject ### {#tt-beh-reject}

*Same as Reject handler for the other Component actors (PatchTracker,
Repository, Workflow, Roadmap, ReleaseTracker) and Project, Organization.*

Meanings:

- An collaborator Invite is being rejected by the invited actor
- A collaborator Join request is being rejected by an existing collaborator

Processes:

- [[#access-via-invite]]
- [[#access-via-join]]

Behavior:

Check the Reject's [=object=].

- If it's an [=Invite=] where [=target=] is my [=collaborators=] collection:
    - Verify the Reject's [=actor=] is the Invite's [=object=]
    - Publish a [=Reject=] on the [=Invite=]

- If it's a [=Join=] without a [=capability=] and where [=object=] is my
    [=collaborators=] collection:
    - Verify the Reject is authorized with [=admin=] access
    - Publish a [=Reject=] on the [=Join=]

### Remove ### {#tt-beh-remove}

*Same as Remove handler for PatchTracker.*

Meanings:

- One of my Collaborators is being removed
- One of my Team Collaborators is being removed
- I'm being removed from one of my Projects

Processes:

- [[#taking-away-access-using-remove-activities]]
- [[#removing-team-resource-link]]
- [[#linking-projects-components]]

Behavior:

- If [=origin=] is my [=collaborators=] collection:
    - Verify the Remove is authorized with [=admin=] access
    - Verify the [=object=] is one of my collaborators
    - Remove the [=object=] actor from my list of collaborators and invalidate
        the direct-Grant I had sent them
    - Publish a [=Revoke=], see [[#taking-away-access-using-remove-activities]]
        step 2.4

- If [=origin=] is my [=context=] (i.e. projects) collection:
    - Verify the Remove is authorized with [=admin=] access
    - Verify the [=object=] is one of the projects in which I'm a component
    - Remove the [=object=] actor from my list of projects and invalidate the
        start-Grant I had sent it
    - Publish an [=Revoke=], see [[#unlinking-projects-components-c]] step 2

- If [=origin=] is my [=teams=] collection:
    - Verify the Remove is authorized with [=admin=] access
    - Verify the [=object=] is one of my Team Collaborators
    - Remove the [=object=] actor from my list of Team Collaborators and
        invalidate the start-Grant I had sent it
    - Publish an [=Accept=], see [[#removing-team-resource-link-r]] step 2

- If I'm the [=object=], being removed from the [=origin=] specifying the
    [=components=] collection of a Project of mine:
        - Just forward to my followers

- If I'm the [=object=], being removed from the [=origin=] specifying the
    [=teamResources=] collection of a Team Collaborator of mine:
        - Nothing to do, just waiting for the Team to send a Revoke on the
            delegate-Grant it had sent me

### Resolve ### {#tt-beh-resolve}

Meanings:

- Someone is requesting to close a ticket

Processes:

- [[#closing-issue]]

Behavior:

- Verify the [=object=] is one of my tickets
- Verify the Resolve is authorized with [=triage=] access
- Mark the ticket as closed in DB
- Publish an Accept, see [[#closing-issue]] step 3

### Undo ### {#tt-beh-undo}

Meanings:

- Someone is asking to unfollow me
- Someone is asking to reopen one of my closed tickets
- A [=Person=] or [=Team=] is being unassigned from one of my tickets

Processes:

- [[#following]]
- [[#reopening-issue]]
- [[#issue-assign]]

Behavior:

Check the Undo's [=object=].

- If it's a [=Follow=] whose [=object=] is me or one of my tickets:
    - Verify the [=actor=] is a follower of the Follow's [=object=]
    - Remove [=actor=] from the list of followers of the Follow's [=object=]
    - Send back an [=Accept=] whose [=object=] points to the [=Undo=]

- It's a [=Resolve=] whose [=object=] is one of my tickets:
    - Verify the Resolve is authorized with [=triage=] access
    - Update the ticket in DB
    - Publish an [=Accept=], see [[#reopening-issue]] step 3

- If it's an [=Assign=] whose [=target=] is one of my tickets:
    - If the [=actor=] is a [=Person=] unassigning themselves:
        - Verify the Undo is authorized with [=visit=] access (unless I'm
            considered a public resource and then the [=capability=] MAY be
            omitted)
        - Remove the [=Relationship=] object from the ticket's [=assignments=]
            collection, see [[#issue-unassign-self]] step 2
        - Send an [=Accept=], see [[#issue-assign-self]] step 3
    - If Undo's [=actor=] and Assign's [=object=] aren't the same actor:
        - Verify the Undo is authorized with [=triage=] access
        - Send an [=Accept=], see [[#issue-unassign-other]] step 2

## PatchTracker ## {#pt-iface}

Vervis implementation:
[Vervis.Actor.Loom](https://codeberg.org/ForgeFed/Vervis/src/branch/main/src/Vervis/Actor/Loom.hs)

### Accept ### {#pt-beh-accept}

Meanings:

- Someone is becoming a Collaborator in me
- A Team is becoming a Team Collaborator in me
- I'm becoming a component of a Project
- Someone is being assigned to one of my tickets
- I'm becoming a collaborator in a [=Workflow=]
- A dependency on one of my tickets is being added or removed
- A milestone is being deleted
- Someone is asking to list a review as being in the name of a team

Processes:

- [[#access-via-invite]]
- [[#access-via-join]]
- [[#forming-team-resource-link]]
- [[#linking-projects-components]]
- [[#issue-assign]]
- [[#s2s-wf-link]]
- [[#s2s-dep]]
- [[#s2s-milestone-delete]]
- [[#s2s-mr-review-team-list]]

Behavior:

Check the Accept's [=object=] as follows.

- If it's an Invite whose [=target=] is my [=collaborators=] collection:
    - Verify Accept [=actor=] is the Invite's [=target=]
    - Verify this collaborator hasn't been enabled already
    - Enable this collaborator in DB
    - Insert the Accept to my inbox & forward to my followers
    - Send a Grant as described in [[#access-via-invite]] step 3

- If it's a Join whose [=object=] is my [=collaborators=] collection:
    - Verify the Accept is authorized with [=admin=] access
    - Verify this collaborator hasn't been enabled already
    - Enable this collaborator in DB
    - Insert the Accept to my inbox & forward to my followers
    - Send a Grant as described in [[#access-via-join]] step 2

- If it's an Add whose [=target=] is my [=teams=] collection,
    i.e. a Team Collaborator is being added to me and the process is
    initiated on *our* side:
    - Error/ignore, we aren't supposed get any Accept

- If it's an Add whose [=object=] is me and [=target=] points to a collection
    whose [=context=] is a [=Team=] that points back to the collection via
    its [=teamResources=] field, i.e. a Team Collaborator is being added to me
    and the process is initiated on *their* side:
    - Option 1: If I haven't yet seen the Team's Accept:
        Verify the Accept's [=actor=] is the Team
    - Option 2: If I saw Team's Acccept but not yet my collaborator's
        Accept: Verify the Accept is authorized with [=admin=] access
    - Otherwise, error/ignore, no Accept is needed
    - For option 1: Record Team's Accept in DB
    - For option 2: Record my collaborator's Accept in DB
    - Insert the Accept to my inbox & forward to my followers
    - For option 2, send an Accept, see [[#forming-team-resource-link-t]]
        step 4

- If it's an Add whose [=target=] is my [=context=] (i.e. projects) collection,
    i.e. I'm being added to a project and the process is initiated on *our*
    side:
    - Nothing to do, just ignore the Accept, waiting for the delegate-Grant
        from the Project

- If it's an Add whose [=object=] is me and [=target=] points to a collection
    whose [=context=] is a [=Project=] that points back to the collection via
    its [=components=] field, i.e. I'm being added to a project and the process
    is initiated on *their* side:
    - If I haven't yet seen the project's approval:
        - Verify the author is the project
        - Insert to my inbox & forward to my followers
    - If I saw project's approval, but not my collaborators' approval:
        - Verify the Accept is authorized with [=admin=] access
        - Insert to my inbox & forward to my followers
        - Publish an Accept, see [[#linking-projects-components-p]] step 4
    - If I already saw both approvals, respond with error

- If it's an [=Assign=] I've seen and authorized, where [=target=] is one of my
    tickets, and where [=actor=] and [=object=] aren't the same actor:
    - Verify the Assign's [=object=] actor isn't already assigned to this
        ticket
    - If the Accept's [=actor=] is the Assign's [=object=]:
        - Insert an item into *t*'s [=assignments=] collection, see
            [[#issue-assign-other]] step 5
        - Send an [=Accept=], see [[#issue-assign-other]] step 6
    - Otherwise:
        - If Assign's [=object=] is a [=Team=]:
            - Just forward to my followers (hopefully this is one of the Team's
                collaborators giving their approval)
        - Otherwise:
            - Ignore/error, not expecting such an Accept

- If it's an Undo I've seen and authorized, whose [=object=] is an [=Assign=]
    where [=target=] is one of my tickets, and where [=actor=] and [=object=]
    aren't the same actor:
    - Verify the Assign's [=object=] is currently assigned to the ticket
    - Verify the Accept's [=actor=] is the Assign's [=object=]
    - Update the ticket's [=assignments=] collection, see
        [[#issue-unassign-other]] step 4
    - Send an Accept, see [[#issue-unassign-other]] step 5

- If it's an Invite I've seen, whose [=object=] is me, and whose [=target=] is
    some [=Workflow=]'s [=collaborators=] collection, and whose [=instrument=]
    is [=visit=]:
    - Verify Accept [=actor=] is the [=Workflow=]
    - Verify I don't have a [=trackerWorkflow=]
    - Send an [=Accept=], see [[#access-via-invite]] step 2

- If it's an [=Offer=] I've seen, of a [=TicketDependency=] where one of my
    tickets is the child:
    - Verify the [=actor=] is the parent ticket's tracker
    - Insert the [=TicketDependency=], referred by the Accept's [=result=], to
        the child's [=dependants=] collection

- If it's an [=Delete=] I've seen, of a [=TicketDependency=] where one of my
    tickets is the child:
    - Verify the [=actor=] is the parent ticket's tracker
    - Remove the [=TicketDependency=] from the child's [=dependants=]
        collection

- If it's a [=Delete=] I've seen, of a [=Milestone=] that belongs to my
    [=trackerRoadmap=]:
    - Update my tickets, unsetting their milestone if they belong to the
        milestone being deleted

- If it's on an [=Add=] I've seen, whose [=object=] refers to an [=Approval=]
    and [=target=] refers to another [=Approval=]:
    - Verify the [=object=] approval is attributed to an individual
    - Verify the [=target=] approval is attributed to a team
    - Verify both approvals are under one of my MRs
    - Verify the Accept's [=actor=] is the team
    - List the individual approval under the team approval's [=attachment=]s
    - Send an [=Accept=], see [[#s2s-mr-review-team-list]] step 4

### Add ### {#pt-beh-add}

Meanings:

- A Team collaborator is being added to me
- I'm being added as a component of a Project
- Someone is asking to mark a MR review as being in the name of a team

Processes:

- [[#forming-team-resource-link]]
- [[#linking-projects-components]]
- [[#s2s-mr-review-team-list]]

Behavior:

- If [=target=] is my [=teams=] collection, i.e. a Team Collaborator is being
    added to me and the process is initiated on *our* side:
    - Verify the [=object=] is a [=Team=]
    - Verify the Add is authorized with [=admin=] access
    - Verify it's not already an active team of mine
    - Insert the Add to my inbox & forward the Add to my followers
    - Publish an Accept, see [[#forming-team-resource-link-r]] step 2

- If [=object=] is me and [=target=] points to a collection
    whose [=context=] is a [=Team=] that points back to the collection via
    its [=teamResources=] field, i.e. a Team Collaborator is being added to me
    and the process is initiated on *their* side:
    - Verify it's not already an active team of mine
    - Insert to my inbox & forward to my followers

- If [=target=] is my [=context=] (i.e. projects) collection, i.e. I'm being
    added to a project and the process is initiated on *our* side:
    - Verify the Add is authorized with [=admin=] access
    - Verify the [=object=] is a [=Project=]
    - Verify I'm not yet a component of this Project
    - Insert to my inbox & forward to my followers
    - Publish an Accept, see [[#linking-projects-components-c]] step 2

- If [=object=] is me and [=target=] points to a collection whose [=context=]
    is a [=Project=] that points back to the collection via its [=components=]
    field, i.e. I'm being added to a project and the process is initiated on
    *their* side:
    - Verify I'm not already a component of this Project
    - Insert to my inbox & forward to my followers

- If [=object=] refers to an [=Approval=] and [=target=] refers to another
    [=Approval=]:
    - Verify the [=object=] approval is attributed to an individual
    - Verify the [=target=] approval is attributed to a team
    - Verify both approvals are under a MR of mine
    - Now just waiting for the team's Accept

### Announce ### {#pt-beh-announce}

Meanings:

- Someone is indicating a pending review on a MR

Processes:

- [[#s2s-mr-review-pending]]

Behavior:

- If [=target=] is me and [=object=] is a [=Review=] object:
    - Verify the action is authorized with [=report=] access
    - Verify the Review's [=context=] is a MR of mine
    - Update my [=Approval=]s, see [[#s2s-mr-review-pending]] step 2
    - Send an [=Accept=], see [[#s2s-mr-review-pending]] step 3

### Assign ### {#pt-beh-assign}

Same as for [=TicketTracker=], see [[#tt-beh-assign]].

### Create ### {#pt-beh-create}

Meanings:

- Someone is commenting on a MR of mine
- Someone is commenting on a review of a MR of mine

Processes:

- [[#commenting]]

Behavior:

- Verify [=object=] is a [=Note=] whose [=context=] is a [=Ticket=] of mine or
    one of its review threads
- Insert to inbox & forward to my followers & MR followers

### Delete ### {#pt-beh-delete}

Meanings:

- Someone is deleting their comment on a MR of mine
- Someone is asking to delete a MR of mine
- Someone is asking to delete me
- Someone is removing a ticket dependency
- A milestone is being deleted

Processes:

- [[#commenting]]
- [[#deleting-mr]]
- [[#deleting-resource-actors]]
- [[#s2s-dep-remove]]
- [[#s2s-milestone-delete]]

Behavior:

- If [=object=] is a [=Note=] whose [=context=] is a [=Ticket=] of mine:
    - Verify sender is the actual author of the Note
    - Insert to inbox & forward to my followers & MR followers

- If [=object=] is a [=Ticket=] of mine:
    - Verify the Delete is authorized with [=admin=] access
    - Delete the MR from DB, such that GET requests for it now return 404 or
        410 status
    - Send an [=Accept=], see [[#deleting-issue]] step 3

- If [=object=] is me:
    - Verify the Delete is authorized with [=admin=] access
    - Send an [=Accept=], see [[#deleting-resource-actors]] step 2
    - Delete myself, such that further GET requests for me or any of my
        objects return 404 or 410 status

- If [=origin=] is me and [=object=] is a [=TicketDependency=]:
    - Verify the Delete is authorized with [=triage=] access
    - Verify it's a [=TicketDependency=] I'm hosting, whose [=subject=] is one
        of my MRs
    - Remove the [=TicketDependency=] from the [=subject=]'s [=dependencies=]
        collection
    - Send an [=Accept=], see [[#s2s-dep-remove]] step 3
    - If the [=TicketDependency=]'s [=object=] is one of my MRs:
        - Remove the [=TicketDependency=] from the [=object=]'s [=dependants=]
            collection

- If [=origin=] isn't me and [=object=] is a [=TicketDependency=]:
    - Verify the [=TicketDependency=]'s [=object=] is one of my MRs
    - Verify the [=TicketDependency=] is listed in the MRs's [=dependants=]
        collection
    - Just insert to my inbox & forward to my followers

- If [=origin=] is my [=trackerRoadmap=] and [=object=] is a [=Milestone=]:
    - Just insert to my inbox & forward to my followers and to the followers of
        every MR of mine that uses this milestone

### Edit ### {#pt-beh-edit}

Meanings:

- Someone is deleting their comment on a MR of mine
- Someone is asking to edit the text of one of my MRs
- Someone is asking to edit the custom fields of one of my tickets
- Someone is asking to edit the milestone of one of my tickets

Processes:

- [[#commenting]]
- [[#editing-ticket]]
- [[#editing-ticket-custom]]
- [[#s2s-edit-ticket-milestone]]

Behavior:

- If [=object=] is a [=Note=] whose [=context=] is a [=Ticket=] of mine:
    - Verify sender is the actual author of the Note
    - Insert to inbox & forward to my followers & MR followers

- If [=object=] is a [=Ticket=] whose [=context=] is me:
    - If [=object=] has [=summary=] and/or [=content=]:
        - Verify the Edit is authorized with [=triage=] access, OR the [=actor=]
            is the same actor as the one who submitted the MR
        - Update the MR in DB from the Edit [=object=]'s fields:
            [=summary=], [=source=], [=content=]
    - If [=object=] specifies only [=customFields=] AND I have a
        [=trackerWorkflow=] set:
        - Verify the Edit is authorized with [=triage=] access
        - For each object listed under [=customFields=]:
            - Verify it has a [=context=] that refers to a [=Field=]
            - Verify it has a [=prop:fieldValue=] specifying a matching value
            - If the value is `null`:
                - Update DB, removing the custom field from the MR
            - Otherwise:
                - Update DB, updating the custom field on the MR
    - If [=object=] has only [=ticketMilestone=]:
        - Verify the Edit is authorized with [=triage=] access
        - Verify I have a [=trackerRoadmap=] set
        - Verify the ticket's new milestone, if set, belongs to that tracker
        - Update the ticket's milestone in DB

### Follow ### {#pt-beh-follow}

Meanings:

- Someone is asking to follow me or a MR of mine

Processes:

- [[#following]]

Behavior:

- Verify the [=object=] is me or one of my tickets
- Verify the [=actor=] isn't already a follower of the [=object=]
- Verify the Follow is authorized with [=visit=] access (unless I'm considered
    a publicly visible resource and then the [=capability=] can be omitted)
- Insert [=actor=] to the list of followers of the [=object=]
- Send back an [=Accept=] whose [=object=] points to the [=Follow=]

### Grant ### {#pt-beh-grant}

Same as for [=TicketTracker=], see [[#tt-beh-grant]].

### Invite ### {#pt-beh-invite}

Meanings:

- Someone is inviting someone else to be a collaborator in me
- Someone is inviting me to be a collaborator in a [=Workflow=]
- Someone is requesting reviews on a MR

Processes:

- [[#access-via-invite]]
- [[#s2s-wf-link]]
- [[#s2s-mr-review-request]]

Behavior:

- If [=target=] is my [=collaborators=] collection:
    - Verify the Invite is authorized with [=admin=] access
    - Verify the [=object=] isn't already a collaborator of me
    - Publish an [=Accept=] whose [=object=] points to the Invite

Note: The Accept in the last step does NOT indicate that a collaborator has
been added. It merely signals to the potential collaborator that the Invite is
authorized, and now they can decide whether to accept/reject/ignore it.

- If [=object=] is me and [=target=] is some [=Workflow=]'s [=collaborators=]
    collection:
    - Verify [=instrument=] is [=visit=]
    - Just insert to my inbox & forward to my followers

- If [=target=] refers to the [=individualApprovals=] and/or [=teamApprovals=]
    collections of a MR of mine:
    - Verify the action is authorized with [=triage=] access
    - Update my [=Approval=]s, see [[#s2s-mr-review-request]] step 2
    - Send an [=Accept=], see [[#s2s-mr-review-request]] step 3

### Join ### {#pt-beh-join}

Same as for [=Factory=], see [[#factory-beh-join]].

### Offer ### {#pt-beh-offer}

Meanings:

- Someone is submitting a new MR
- Someone is submitting a ticket dependency
- Someone is submitting a MR review
- A team is submitting a MR approval

Processes:

- [[#opening-mr]]
- [[#s2s-dep-add]]
- [[#s2s-mr-review]]
- [[#s2s-mr-review-team-approve]]

Behavior:

- If [=target=] is me and [=object=] is a [=Ticket=] with an [=attachment=],
    see [[#opening-mr]] step 3:
    - Create new MR in DB
    - Publish an [=Accept=] on the [=Offer=], see [[#opening-mr]]

- If [=target=] is me and [=object=] is a [=TicketDependency=]:
    - Verify the Offer is authorized with [=triage=] access
    - Verify the [=TicketDependency=]'s [=subject=] is one of my MRs
    - Verify the [=TicketDependency=]'s [=relationship=] is [=dependsOn=]
    - Insert the [=TicketDependency=] to the [=subject=]'s [=dependencies=]
        collection
    - Send an [=Accept=], see [[#s2s-dep-add]] step 3
    - If the [=TicketDependency=]'s [=object=] is one of my MRs:
        - Insert the [=TicketDependency=] to the [=object=]'s [=dependants=]
            collection

- If [=target=] isn't me and [=object=] is a [=TicketDependency=]:
    - Verify the [=TicketDependency=]'s [=object=] is one of my MRs
    - Verify the [=TicketDependency=]'s [=relationship=] is [=dependsOn=]
    - Just insert to my inbox & forward to my followers

- If [=target=] is me, [=object=] is a [=Review=] and [=actor=] is a
    [=Person=]:
    - Verify the action is authorized with [=report=] access
    - If [=reviewIsBinding=] is `true`, require [=write=] access
    - Verify the Review's [=object=] matches the latest version of the MR (the
        MR itself is referred via the Review's [=context=])
    - For each of the Review's [=reviewThreads=], find and specify the right
        diff hunk via [=cqHunk=]
    - Assign [=id=] URIs to the Review and the ReviewThreads
    - Update the MR's [=individualApprovals=] collection, adding or updating
        the item for [=actor=]
    - Sends an [=Accept=], see [[#s2s-mr-review]] step 6

- If [=target=] is me, [=object=] is a [=Review=] object and [=actor=] is a
    [=Team=]:
    - Verify the Review's [=context=] refers to one of my MRs
    - Verify the Review's [=verdict=] is [=approve=]
    - Verify the Review's [=object=] matches the latest version of the MR
    - Verify I already have an [=Approval=] attributed to this [=Team=]
    - Assign an [=id=] URI to the Review
    - Update the approval's status
    - Send an [=Accept=], see [[#s2s-mr-review-team-approve]] step 4

### Reject ### {#pt-beh-reject}

Same as for [=TicketTracker=], see [[#tt-beh-reject]].

### Remove ### {#pt-beh-remove}

Same as for [=TicketTracker=], see [[#tt-beh-remove]].

### Revoke ### {#pt-beh-revoke}

Same as for [=TicketTracker=], see [[#tt-beh-revoke]].

### Resolve ### {#pt-beh-resolve}

Meanings:

- Someone is requesting to close an open MR
- Someone is requesting to resolve a review thread

Processes:

- [[#closing-mr]]
- [[#s2s-mr-review-resolve]]

Behavior:

- If [=object=] is one of my MRs:
    - Verify the Resolve is authorized with [=triage=] access
    - Mark the MR as closed in DB
    - Publish an Accept, see [[#closing-issue]] step 3

- If [=object=] is a [=ReviewThread=] under a [=Review=] under one of my MRs:
    - Verify the action is authorized with [=write=] access
    - Update the thread's [=isResolved=]
    - Send an [=Accept=], see [[#s2s-mr-review-resolve]] step 3

### Undo ### {#pt-beh-undo}

Meanings:

- Someone is asking to unfollow me
- Someone is asking to reopen one of my closed (but not merged) MRs
- A [=Person=] or [=Team=] is being unassigned from one of my MRs
- Someone is asking to unresolve a review thread
- Someone is asking to dismiss a review

Processes:

- [[#following]]
- [[#reopening-mr]]
- [[#mr-assign]]
- [[#s2s-mr-review-unresolve]]
- [[#s2s-mr-review-dismiss]]

Behavior:

Check the Undo's [=object=].

- If it's a [=Follow=] whose [=object=] is me or one of my MRs:
    - Verify the [=actor=] is a follower of the Follow's [=object=]
    - Remove [=actor=] from the list of followers of the Follow's [=object=]
    - Send back an [=Accept=] whose [=object=] points to the [=Undo=]

It's a [=Resolve=] whose [=object=] is one of my closed but not merged MRs:
    - Verify the Resolve is authorized with [=triage=] access
    - Update the MR in DB
    - Publish an [=Accept=], see [[#reopening-issue]] step 3

- If it's an [=Assign=] whose [=target=] is one of my MRs:
    - If the [=actor=] is a [=Person=] unassigning themselves:
        - Verify the Undo is authorized with [=visit=] access (unless I'm
            considered a public resource and then the [=capability=] MAY be
            omitted)
        - Remove the [=Relationship=] object from the MR's [=assignments=]
            collection, see [[#issue-unassign-self]] step 2
        - Send an [=Accept=], see [[#issue-assign-self]] step 3
    - If Undo's [=actor=] and Assign's [=object=] aren't the same actor:
        - Verify the Undo is authorized with [=triage=] access
        - Send an [=Accept=], see [[#issue-unassign-other]] step 2

- If it's a [=ReviewThread=] under a [=Review=] under one of my MRs:
    - Verify the action is authorized with [=write=] access
    - Verify the thread is resolved
    - Set its [=isResolved=] to `false`
    - Send an [=Accept=], see [[#s2s-mr-review-unresolve]] step 3

- If it's an [=Approval=] under one of my MRs:
    - Verify the action is authorized with [=write=] access
    - Update the approval's status, see [[#s2s-mr-review-dismiss]] step 2
    - Send ann [=Accept=], see [[#s2s-mr-review-dismiss]] step 3

## Workflow ## {#wf-beh}

Vervis implementation: None currently

### Accept ### {#wf-beh-accept}

Meanings:

- Someone is becoming a Collaborator in me
- A Team is becoming a Team Collaborator in me
- I'm becoming a component of a Project

Processes:

- [[#access-via-invite]]
- [[#access-via-join]]
- [[#forming-team-resource-link]]
- [[#linking-projects-components]]

Behavior:

Check the Accept's [=object=] as follows.

- If it's an Invite whose [=target=] is my [=collaborators=] collection:
    - Verify Accept [=actor=] is the Invite's [=object=]
    - Verify this collaborator hasn't been enabled already
    - Enable this collaborator in DB
    - If [=instrument=] is [=visit=] and [=object=] is a [=TicketTracker=] or a
        [=PatchTracker=]:
        - Insert the [=object=] to my [=workflowTrackers=] collection
    - Send a Grant as described in [[#access-via-invite]] step 3

- If it's a Join whose [=object=] is my [=collaborators=] collection:
    - Verify the Accept is authorized with [=admin=] access
    - Verify this collaborator hasn't been enabled already
    - Enable this collaborator in DB
    - If [=instrument=] is [=visit=] and [=actor=] is a [=TicketTracker=] or a
        [=PatchTracker=]:
        - Insert the [=actor=] to my [=workflowTrackers=] collection
    - Send a Grant as described in [[#access-via-join]] step 2

- If it's an Add whose [=target=] is my [=teams=] collection,
    i.e. a Team Collaborator is being added to me and the process is
    initiated on *our* side:
    - Error/ignore, we aren't supposed get any Accept

- If it's an Add whose [=object=] is me and [=target=] points to a collection
    whose [=context=] is a [=Team=] that points back to the collection via
    its [=teamResources=] field, i.e. a Team Collaborator is being added to me
    and the process is initiated on *their* side:
    - Option 1: If I haven't yet seen the Team's Accept:
        Verify the Accept's [=actor=] is the Team
    - Option 2: If I saw Team's Acccept but not yet my collaborator's
        Accept: Verify the Accept is authorized with [=admin=] access
    - Otherwise, error/ignore, no Accept is needed
    - For option 1: Record Team's Accept in DB
    - For option 2: Record my collaborator's Accept in DB
    - Insert the Accept to my inbox & forward to my followers
    - For option 2, send an Accept, see [[#forming-team-resource-link-t]]
        step 4

- If it's an Add whose [=target=] is my [=context=] (i.e. projects) collection,
    i.e. I'm being added to a project and the process is initiated on *our*
    side:
    - Nothing to do, just ignore the Accept, waiting for the delegate-Grant
        from the Project

- If it's an Add whose [=object=] is me and [=target=] points to a collection
    whose [=context=] is a [=Project=] that points back to the collection via
    its [=components=] field, i.e. I'm being added to a project and the process
    is initiated on *their* side:
    - If I haven't yet seen the project's approval:
        - Verify the author is the project
        - Insert to my inbox & forward to my followers
    - If I saw project's approval, but not my collaborators' approval:
        - Verify the Accept is authorized with [=admin=] access
        - Insert to my inbox & forward to my followers
        - Publish an Accept, see [[#linking-projects-components-p]] step 4
    - If I already saw both approvals, respond with error

### Add ### {#wf-beh-add}

Meanings:

- A Team collaborator is being added to me
- I'm being added as a component of a Project
- An [=EnumValue=] is being added to one of my [=Enum=]s

Processes:

- [[#forming-team-resource-link]]
- [[#linking-projects-components]]
- [[#s2s-wf-enum-add]]

Behavior:

- If [=target=] is my [=teams=] collection, i.e. a Team Collaborator is being
    added to me and the process is initiated on *our* side:
    - Verify the [=object=] is a [=Team=]
    - Verify the Add is authorized with [=admin=] access
    - Verify it's not already an active team of mine
    - Insert the Add to my inbox & forward the Add to my followers
    - Publish an Accept, see [[#forming-team-resource-link-r]] step 2

- If [=object=] is me and [=target=] points to a collection
    whose [=context=] is a [=Team=] that points back to the collection via
    its [=teamResources=] field, i.e. a Team Collaborator is being added to me
    and the process is initiated on *their* side:
    - Verify it's not already an active team of mine
    - Insert to my inbox & forward to my followers

- If [=target=] is my [=context=] (i.e. projects) collection, i.e. I'm being
    added to a project and the process is initiated on *our* side:
    - Verify the Add is authorized with [=admin=] access
    - Verify the [=object=] is a [=Project=]
    - Verify I'm not yet a component of this Project
    - Insert to my inbox & forward to my followers
    - Publish an Accept, see [[#linking-projects-components-c]] step 2

- If [=object=] is me and [=target=] points to a collection whose [=context=]
    is a [=Project=] that points back to the collection via its [=components=]
    field, i.e. I'm being added to a project and the process is initiated on
    *their* side:
    - Verify I'm not already a component of this Project
    - Insert to my inbox & forward to my followers

- If [=target=] is my [=enumValues=] collection:
    - Verify the Add is authorized with [=maintain=] access
    - Verify [=object=] is a valid [=EnumValue=]
    - Insert the [=EnumValue=] to my [=enumValues=] collection
    - Send an [=Accept=], see [[#s2s-wf-enum-add]] step 3

### Delete ### {#wf-beh-delete}

Meanings:

- Someone is asking to delete an [=Enum=] of mine
- Someone is asking to delete an [=Field=] of mine
- Someone is asking to delete me

Processes:

- [[#s2s-enum-delete]]
- [[#s2s-field-delete]]
- [[#deleting-resource-actors]]

Behavior:

- If [=object=] is an [=Enum=] of mine and [=origin=] is me:
    - Verify the Delete is authorized with [=maintain=] access
    - Verify none of my [=Field=]s are using this [=Enum=]
    - Delete the Enum from DB, such that GET requests for it now return 404 or
        410 status
    - Send an [=Accept=], see [[#s2s-enum-delete]] step 4

- If [=object=] is an [=Field=] of mine and [=origin=] is me:
    - Verify the Delete is authorized with [=maintain=] access
    - Delete the Field from DB, such that GET requests for it now return 404 or
        410 status
    - Send an [=Accept=], see [[#s2s-field-delete]] step 3

- If [=object=] is me:
    - Verify the Delete is authorized with [=admin=] access
    - Send an [=Accept=], see [[#deleting-resource-actors]] step 2
    - Delete myself, such that further GET requests for me or any of my
        objects return 404 or 410 status

### Edit ### {#wf-beh-edit}

Meanings:

- Someone is asking to edit my name and/or description

Processes:

- [[#editing-workflow]]

Behavior:

Check the [=object=]'s [=id=].

- If it's me:
    - Verify the Edit is authorized with [=admin=] access
    - Update me in DB from the Edit [=object=]'s fields: [=name=], [=summary=]

- If it's a [=Field=] of mine:
    - Verify the Edit is authorized with [=maintain=] access
    - Update the [=Field=] in DB from the Edit [=object=]'s fields: [=name=],
        [=summary=], [=fieldColor=]

- If it's an [=Enum=] of mine:
    - Verify the Edit is authorized with [=maintain=] access
    - Update the [=Enum=] in DB from the Edit [=object=]'s fields: [=name=],
        [=summary=], [=enumIsOrdered=]. If [=enumIsOrdered=] is being set to
        `false`, order the values by the order in which the values were
        created, first to last.

- If it's an [=EnumValue=] of mine:
    - Verify the Edit is authorized with [=maintain=] access
    - Update the [=EnumValue=] in DB from the Edit [=object=]'s fields:
        [=name=], [=summary=], [=enumValueColor=]

### Follow ### {#wf-beh-follow}

Meanings:

- Someone is asking to follow me

Processes:

- [[#following]]

Behavior:

- Verify the [=object=] is me
- Verify the [=actor=] isn't already a follower of the [=object=]
- Verify the Follow is authorized with [=visit=] access (unless I'm considered
    a publicly visible resource and then the [=capability=] can be omitted)
- Insert [=actor=] to the list of followers of the [=object=]
- Send back an [=Accept=] whose [=object=] points to the [=Follow=]

### Grant ### {#wf-beh-grant}

Meanings:

- A Project-in-process is sending me the delegate-Grant
- A Team-Collaborator-in-process is sending me the delegate-Grant

Processes:

- [[#linking-projects-components]]
- [[#forming-team-resource-link]]

Behavior:

Check the Grant's [=fulfills=] field, verify it's an [=Add=] activity that I've
seen:

- If it's a Project-in-process which is now sending me the delegate-Grant, and
    the previous steps in the process successfully happened:
    - Insert the Project to my list of Projects and enable in DB
    - Send the Project a start-Grant, see [[#linking-projects-components-c]]
        step 5 or [[#linking-projects-components-p]] step 6

- If it's a Team-Collaborator-in-process which is now sending me the
    delegate-Grant, and the previous steps in the process successfully
    happened:
    - Insert the Team to my list of Team Collaborators and enable in DB
    - Send the Team a start-Grant, see [[#deleg-step-tr]] step 1

### Invite ### {#wf-beh-invite}

Same as for [=Factory=], see [[#factory-beh-invite]].

### Join ### {#wf-beh-join}

Same as for [=Factory=], see [[#factory-beh-join]].

### Move ### {#wf-beh-move}

Meanings:

- I'm requested to update the order of values in one of my [=Enum=]s
- I'm requested to update the order of my [=Fields=]s

Processes:

- [[#s2s-wf-enum-reorder]]
- [[#s2s-wf-field-reorder]]

Behavior:

- If [=object=] is an [=EnumValue=] within one of my [=Enum=]s:
    - Verify the Move is authorized with [=maintain=] access
    - Verify the Enum's [=enumIsOrdered=] is `true`
    - If [=originPosition=] is specified, verify it's the current 1-based
        position of the value referred by [=object=]
    - If [=target=] is specified, verify it refers to the item that is
        currently at the 1-based position pointed by [=targetPositionAfter=]
    - Update the Enum's value list, moving the value to the position after the
        1-based index indicated by [=targetPositionAfter=] (0 means move to the
        beginning)
    - Send an [=Accept=], see [[#s2s-wf-enum-reorder]] step 3

- If [=object=] is one of my [=Field=]s:
    - Verify the Move is authorized with [=maintain=] access
    - If [=originPosition=] is specified, verify it's the current 1-based
        position of the field referred by [=object=]
    - If [=target=] is specified, verify it refers to the item that is
        currently at the 1-based position pointed by [=targetPositionAfter=]
    - Update my fields list, moving the field to the position after the 1-based
        index indicated by [=targetPositionAfter=] (0 means move to the
        beginning)
    - Send an [=Accept=], see [[#s2s-wf-field-reorder]] step 3

### Offer ### {#wf-beh-offer}

Meanings:

- Someone is submitting a new [=Enum=]
- Someone is submitting a new [=Field=]

Processes:

- [[#s2s-wf-enum-create]]
- [[#s2s-wf-field-create]]

Behavior:

- If [=target=] is me and [=object=] is an [=Enum=]:
    - Verify the Offer is authorized with [=maintain=] access
    - Store new Enum in DB
    - Publish an [=Accept=], see [[#s2s-wf-enum-create]] step 3

- If [=target=] is me and [=object=] is a [=Field=]:
    - Verify the Offer is authorized with [=maintain=] access
    - Store new Field in DB
    - Publish an [=Accept=], see [[#s2s-wf-field-create]] step 3

### Reject ### {#wf-beh-reject}

Same as for [=TicketTracker=], see [[#tt-beh-reject]].

### Remove ### {#wf-beh-remove}

Meanings:

- One of my Collaborators is being removed
- One of my Team Collaborators is being removed
- I'm being removed from one of my Projects
- One of my [=EnumValue=]s is being removed

Processes:

- [[#taking-away-access-using-remove-activities]]
- [[#removing-team-resource-link]]
- [[#linking-projects-components]]
- [[#s2s-wf-enum-remove]]

Behavior:

- If [=origin=] is my [=collaborators=] collection:
    - Verify the Remove is authorized with [=admin=] access
    - Verify the [=object=] is one of my collaborators
    - Remove the [=object=] actor from my list of collaborators and invalidate
        the direct-Grant I had sent them
    - If [=object=] appears in my [=workflowTrackers=] collection, remove it
        from there
    - Publish a [=Revoke=], see [[#taking-away-access-using-remove-activities]]
        step 2.4

- If [=origin=] is my [=context=] (i.e. projects) collection:
    - Verify the Remove is authorized with [=admin=] access
    - Verify the [=object=] is one of the projects in which I'm a component
    - Remove the [=object=] actor from my list of projects and invalidate the
        start-Grant I had sent it
    - Publish an [=Revoke=], see [[#unlinking-projects-components-c]] step 2

- If [=origin=] is my [=teams=] collection:
    - Verify the Remove is authorized with [=admin=] access
    - Verify the [=object=] is one of my Team Collaborators
    - Remove the [=object=] actor from my list of Team Collaborators and
        invalidate the start-Grant I had sent it
    - Publish an [=Accept=], see [[#removing-team-resource-link-r]] step 2

- If I'm the [=object=], being removed from the [=origin=] specifying the
    [=components=] collection of a Project of mine:
        - Just forward to my followers

- If I'm the [=object=], being removed from the [=origin=] specifying the
    [=teamResources=] collection of a Team Collaborator of mine:
        - Nothing to do, just waiting for the Team to send a Revoke on the
            delegate-Grant it had sent me

- If [=origin=] is the [=enumValues=] collection of one of my [=Enum=]s:
    - Verify [=object=] refers to one of the [=EnumValues=] of that [=Enum=]
    - Verify the Remove is authorized with [=maintain=] access
    - Remove the value from the Enum's list of values
    - Send an [=Accept=], see [[#s2s-wf-enum-remove]] step 3

### Undo ### {#wf-beh-undo}

Meanings:

- Someone is asking to unfollow me

Processes:

- [[#following]]

Behavior:

Check the Undo's [=object=].

- If it's a [=Follow=] whose [=object=] is me or one of my MRs:
    - Verify the [=actor=] is a follower of the Follow's [=object=]
    - Remove [=actor=] from the list of followers of the Follow's [=object=]
    - Send back an [=Accept=] whose [=object=] points to the [=Undo=]

## Roadmap ## {#roadmap-beh}

Vervis implementation: None currently

### Accept ### {#roadmap-beh-accept}

Meanings:

- Someone is becoming a Collaborator in me
- A Team is becoming a Team Collaborator in me
- I'm becoming a component of a Project
- A ticket that uses one of my milestones is being deleted
- The milestone of a ticket is being edited
- One of my milestones is being associated or unassociated with a release
- A release associated with a milestone(s) of mine is being deleted

Processes:

- [[#access-via-invite]]
- [[#access-via-join]]
- [[#forming-team-resource-link]]
- [[#linking-projects-components]]
- [[#deleting-issue]]
- [[#s2s-edit-ticket-milestone]]
- [[#s2s-release-milestone-add]]
- [[#s2s-release-milestone-remove]]
- [[#s2s-release-delete]]

Behavior:

Check the Accept's [=object=] as follows.

- If it's an Invite whose [=target=] is my [=collaborators=] collection:
    - Verify Accept [=actor=] is the Invite's [=object=]
    - Verify this collaborator hasn't been enabled already
    - Enable this collaborator in DB
    - If [=instrument=] is [=visit=] and [=object=] is a [=TicketTracker=] or a
        [=PatchTracker=] or a [=ReleaseTracker=]:
        - Insert the [=object=] to my [=roadmapTrackers=] collection
    - Send a Grant as described in [[#access-via-invite]] step 3

- If it's a Join whose [=object=] is my [=collaborators=] collection:
    - Verify the Accept is authorized with [=admin=] access
    - Verify this collaborator hasn't been enabled already
    - Enable this collaborator in DB
    - If [=instrument=] is [=visit=] and [=actor=] is a [=TicketTracker=] or a
        [=PatchTracker=] or a [=ReleaseTracker=]:
        - Insert the [=actor=] to my [=roadmapTrackers=] collection
    - Send a Grant as described in [[#access-via-join]] step 2

- If it's an Add whose [=target=] is my [=teams=] collection,
    i.e. a Team Collaborator is being added to me and the process is
    initiated on *our* side:
    - Error/ignore, we aren't supposed get any Accept

- If it's an Add whose [=object=] is me and [=target=] points to a collection
    whose [=context=] is a [=Team=] that points back to the collection via
    its [=teamResources=] field, i.e. a Team Collaborator is being added to me
    and the process is initiated on *their* side:
    - Option 1: If I haven't yet seen the Team's Accept:
        Verify the Accept's [=actor=] is the Team
    - Option 2: If I saw Team's Acccept but not yet my collaborator's
        Accept: Verify the Accept is authorized with [=admin=] access
    - Otherwise, error/ignore, no Accept is needed
    - For option 1: Record Team's Accept in DB
    - For option 2: Record my collaborator's Accept in DB
    - Insert the Accept to my inbox & forward to my followers
    - For option 2, send an Accept, see [[#forming-team-resource-link-t]]
        step 4

- If it's an Add whose [=target=] is my [=context=] (i.e. projects) collection,
    i.e. I'm being added to a project and the process is initiated on *our*
    side:
    - Nothing to do, just ignore the Accept, waiting for the delegate-Grant
        from the Project

- If it's an Add whose [=object=] is me and [=target=] points to a collection
    whose [=context=] is a [=Project=] that points back to the collection via
    its [=components=] field, i.e. I'm being added to a project and the process
    is initiated on *their* side:
    - If I haven't yet seen the project's approval:
        - Verify the author is the project
        - Insert to my inbox & forward to my followers
    - If I saw project's approval, but not my collaborators' approval:
        - Verify the Accept is authorized with [=admin=] access
        - Insert to my inbox & forward to my followers
        - Publish an Accept, see [[#linking-projects-components-p]] step 4
    - If I already saw both approvals, respond with error

- If it's a [=Delete=] I've seen, whose [=object=] refers to a [=Ticket=]:
    - Verify the ticket belongs to one of my trackers
    - Verify the ticket's milestone is one of my milestones
    - Remove the ticket from that milestone's [=milestoneTickets=] collection

- If it's an [=Edit=] I've seen, where a ticket's milestone is being edited:
    - Verify the ticket belongs to one of my trackers
    - Verify its old and new milestones are mine
    - It the ticket was in the [=milestoneTickets=] collection of one of my
        milestones, remove it from there
    - If the new [=ticketMilestone=] value isn't `null`, add the ticket to the
        [=milestoneTickets=] collection of the new chosen milestone

- If it's an [=Add=] I've seen, where [=object=] is one of my milestones and
    [=target=] is the [=releaseMilestones=] collection of a [=Release=]:
    - Verify the release's [=context=] belong to one of my [=roadmapTrackers=]
    - Verify [=actor=] is that [=ReleaseTracker=]
    - Insert the release to the milestone's [=milestoneReleases=] collection

- If it's an [=Remove=] I've seen, where [=object=] is one of my milestones and
    [=origin=] is the [=releaseMilestones=] collection of a [=Release=]:
    - Verify the release's [=context=] belong to one of my [=roadmapTrackers=]
    - Verify [=actor=] is that [=ReleaseTracker=]
    - Verify the release appears in the milestone's [=milestoneReleases=]
        collection
    - Remove the release from the milestone's [=milestoneReleases=] collection

- If it's a [=Delete=] I've seen, whose [=object=] is a [=Release=]:
    - Verify the release's [=context=] is one of my [=roadmapTrackers=]
    - Verify [=actor=] is that [=ReleaseTracker=]
    - Verify the release is associated with at least one milestone of mine
    - Remove the release from the [=milestoneReleases=] collections of all such
        milestones

### Add ### {#roadmap-beh-add}

Meanings:

- A Team collaborator is being added to me
- I'm being added as a component of a Project
- One of my milestones is being associated with a release

Processes:

- [[#forming-team-resource-link]]
- [[#linking-projects-components]]
- [[#s2s-release-milestone-add]]

Behavior:

- If [=target=] is my [=teams=] collection, i.e. a Team Collaborator is being
    added to me and the process is initiated on *our* side:
    - Verify the [=object=] is a [=Team=]
    - Verify the Add is authorized with [=admin=] access
    - Verify it's not already an active team of mine
    - Insert the Add to my inbox & forward the Add to my followers
    - Publish an Accept, see [[#forming-team-resource-link-r]] step 2

- If [=object=] is me and [=target=] points to a collection
    whose [=context=] is a [=Team=] that points back to the collection via
    its [=teamResources=] field, i.e. a Team Collaborator is being added to me
    and the process is initiated on *their* side:
    - Verify it's not already an active team of mine
    - Insert to my inbox & forward to my followers

- If [=target=] is my [=context=] (i.e. projects) collection, i.e. I'm being
    added to a project and the process is initiated on *our* side:
    - Verify the Add is authorized with [=admin=] access
    - Verify the [=object=] is a [=Project=]
    - Verify I'm not yet a component of this Project
    - Insert to my inbox & forward to my followers
    - Publish an Accept, see [[#linking-projects-components-c]] step 2

- If [=object=] is me and [=target=] points to a collection whose [=context=]
    is a [=Project=] that points back to the collection via its [=components=]
    field, i.e. I'm being added to a project and the process is initiated on
    *their* side:
    - Verify I'm not already a component of this Project
    - Insert to my inbox & forward to my followers

- If [=object=] is one of my milestones and [=target=] is the
    [=releaseMilestones=] collection of a [=Release=]:
    - Verify the release's [=context=] is one of my [=roadmapTrackers=]
    - Just insert to my inbox & forward to my followers and milestone followers

### Delete ### {#roadmap-beh-delete}

Meanings:

- Someone is asking to delete a [=Milestone=] of mine
- Someone is asking to delete me
- Someone is asking to delete a ticket that belongs to a milestone of mine
- A release associated with a milestone(s) of mine is being deleted

Processes:

- [[#s2s-milestone-delete]]
- [[#deleting-resource-actors]]
- [[#deleting-issue]]
- [[#s2s-release-delete]]

Behavior:

Check the [=object=].

- If it's a [=Milestone=] of mine and [=origin=] is me:
    - Verify the Delete is authorized with [=write=] access
    - Delete the milestone from DB, such that GET requests for it now return
        404 or 410 status
    - Send an [=Accept=], see [[#s2s-milestone-delete]] step 3

- If it's me:
    - Verify the Delete is authorized with [=admin=] access
    - Send an [=Accept=], see [[#deleting-resource-actors]] step 2
    - Delete myself, such that further GET requests for me or any of my
        objects return 404 or 410 status

- If it's a [=Ticket=]:
    - Verify the ticket belongs to one of my trackers
    - Verify the ticket's milestone is one of my milestones
    - Just insert to my inbox & forward to my followers and milestone
        followers, now waiting for the tracker's Accept

- If it's a [=Release=]:
    - Verify it belongs to one of my [=roadmapTrackers=]
    - Verify I have at least one milestone associated with this release
    - Just insert to my inbox & forward to my followers and milestone followers

### Edit ### {#roadmap-beh-edit}

Meanings:

- Someone is asking to edit my name and/or description
- Someone is asking to edit one of my milestones
- Someone is editing a ticket's milestone

Processes:

- [[#s2s-edit-roadmap]]
- [[#s2s-edit-milestone]]
- [[#s2s-edit-ticket-milestone]]

Behavior:

Check the [=object=]'s [=id=].

- If it's me:
    - Verify the Edit is authorized with [=admin=] access
    - Update me in DB from the Edit [=object=]'s fields: [=name=], [=summary=]

- If it's a [=Milestone=] of mine:
    - Verify the Edit is authorized with [=maintain=] access
    - Update the [=Field=] in DB from the Edit [=object=]'s fields: [=name=],
        [=content=], [=source=], [=startTime=], [=endTime=]

- If it's a [=Ticket=]:
    - Verify the [=ticketMilestone=] is being edited
    - Verify it belongs to one of my trackers
    - Verify its old and new milestones are mine
    - Just insert to my inbox and forward to my followers + old and new
        milestones' followers, now waiting for tracker's Accept

### Follow ### {#roadmap-beh-follow}

Meanings:

- Someone is asking to follow me
- Someone is asking to follow one of my milestones

Processes:

- [[#following]]

Behavior:

- Verify the [=object=] is me or one of my milestones
- Verify the [=actor=] isn't already a follower of the [=object=]
- Verify the Follow is authorized with [=visit=] access (unless I'm considered
    a publicly visible resource and then the [=capability=] can be omitted)
- Insert [=actor=] to the list of followers of the [=object=]
- Send back an [=Accept=] whose [=object=] points to the [=Follow=]

### Grant ### {#roadmap-beh-grant}

Meanings:

- A Project-in-process is sending me the delegate-Grant
- A Team-Collaborator-in-process is sending me the delegate-Grant

Processes:

- [[#linking-projects-components]]
- [[#forming-team-resource-link]]

Behavior:

Check the Grant's [=fulfills=] field, verify it's an [=Add=] activity that I've
seen:

- If it's a Project-in-process which is now sending me the delegate-Grant, and
    the previous steps in the process successfully happened:
    - Insert the Project to my list of Projects and enable in DB
    - Send the Project a start-Grant, see [[#linking-projects-components-c]]
        step 5 or [[#linking-projects-components-p]] step 6

- If it's a Team-Collaborator-in-process which is now sending me the
    delegate-Grant, and the previous steps in the process successfully
    happened:
    - Insert the Team to my list of Team Collaborators and enable in DB
    - Send the Team a start-Grant, see [[#deleg-step-tr]] step 1

### Invite ### {#roadmap-beh-invite}

Same as for [=Factory=], see [[#factory-beh-invite]].

### Join ### {#roadmap-beh-join}

Same as for [=Factory=], see [[#factory-beh-join]].

### Offer ### {#roadmap-beh-offer}

Meanings:

- Someone is submitting a new [=Milestone=]

Processes:

- [[#s2s-milestone-create]]

Behavior:

- If [=target=] is me and [=object=] is a [=Milestone=]:
    - Verify the Offer is authorized with [=write=] access
    - Store new milestone in DB
    - Publish an [=Accept=], see [[#s2s-milestone-create]] step 3

### Reject ### {#roadmap-beh-reject}

Same as for [=TicketTracker=], see [[#tt-beh-reject]].

### Remove ### {#roadmap-beh-remove}

Meanings:

- One of my Collaborators is being removed
- One of my Team Collaborators is being removed
- I'm being removed from one of my Projects
- One of my milestones is being unassigned from a release

Processes:

- [[#taking-away-access-using-remove-activities]]
- [[#removing-team-resource-link]]
- [[#linking-projects-components]]
- [[#s2s-release-milestone-remove]]

Behavior:

- If [=origin=] is my [=collaborators=] collection:
    - Verify the Remove is authorized with [=admin=] access
    - Verify the [=object=] is one of my collaborators
    - Remove the [=object=] actor from my list of collaborators and invalidate
        the direct-Grant I had sent them
    - If [=object=] appears in my [=roadmapTrackers=] collection, remove it
        from there
    - Publish a [=Revoke=], see [[#taking-away-access-using-remove-activities]]
        step 2.4

- If [=origin=] is my [=context=] (i.e. projects) collection:
    - Verify the Remove is authorized with [=admin=] access
    - Verify the [=object=] is one of the projects in which I'm a component
    - Remove the [=object=] actor from my list of projects and invalidate the
        start-Grant I had sent it
    - Publish an [=Revoke=], see [[#unlinking-projects-components-c]] step 2

- If [=origin=] is my [=teams=] collection:
    - Verify the Remove is authorized with [=admin=] access
    - Verify the [=object=] is one of my Team Collaborators
    - Remove the [=object=] actor from my list of Team Collaborators and
        invalidate the start-Grant I had sent it
    - Publish an [=Accept=], see [[#removing-team-resource-link-r]] step 2

- If I'm the [=object=], being removed from the [=origin=] specifying the
    [=components=] collection of a Project of mine:
        - Just forward to my followers

- If I'm the [=object=], being removed from the [=origin=] specifying the
    [=teamResources=] collection of a Team Collaborator of mine:
        - Nothing to do, just waiting for the Team to send a Revoke on the
            delegate-Grant it had sent me

- If [=object=] is one of my milestones and [=origin=] is the
    [=releaseMilestones=] collection of a [=Release=]:
    - Verify the release's [=context=] is one of my [=roadmapTrackers=]
    - Verify the release is listed in the milestone's [=milestoneReleases=]
        collection
    - Just insert to my inbox & forward to my followers and milestone followers

### Resolve ### {#roadmap-beh-resolve}

Meanings:

- Someone is requesting to close a milestone

Processes:

- [[#s2s-milestone-close]]

Behavior:

- Verify the [=object=] is one of my milestones
- Verify the Resolve is authorized with [=write=] access
- Mark the milestone as closed in DB
- Publish an Accept, see [[#s2s-milestone-close]] step 3

### Undo ### {#roadmap-beh-undo}

Meanings:

- Someone is asking to unfollow me or one of my milestones
- Someone is asking to reopen one of my closed milestones

Processes:

- [[#following]]
- [[#s2s-milestone-reopen]]

Behavior:

Check the Undo's [=object=].

- If it's a [=Follow=] whose [=object=] is me or one of my milestones:
    - Verify the [=actor=] is a follower of the Follow's [=object=]
    - Remove [=actor=] from the list of followers of the Follow's [=object=]
    - Send back an [=Accept=] whose [=object=] points to the [=Undo=]

- It's a [=Resolve=] whose [=object=] is one of my milestones:
    - Verify the Resolve is authorized with [=write=] access
    - Update the milestone in DB
    - Publish an [=Accept=], see [[#s2s-milestone-reopen]] step 3

## ReleaseTracker ## {#rt-beh}

Vervis implementation: None currently

### Accept ### {#rt-beh-accept}

Meanings:

- Someone is becoming a Collaborator in me
- A Team is becoming a Team Collaborator in me
- I'm becoming a component of a Project
- I'm becoming a collaborator in a [=Repository=]
- A milestone is being deleted

Processes:

- [[#access-via-invite]]
- [[#access-via-join]]
- [[#forming-team-resource-link]]
- [[#linking-projects-components]]
- [[#s2s-release-tracker-repo-link]]
- [[#s2s-milestone-delete]]

Behavior:

Check the Accept's [=object=] as follows.

- If it's an Invite whose [=target=] is my [=collaborators=] collection:
    - Verify Accept [=actor=] is the Invite's [=target=]
    - Verify this collaborator hasn't been enabled already
    - Enable this collaborator in DB
    - Insert the Accept to my inbox & forward to my followers
    - Send a Grant as described in [[#access-via-invite]] step 3

- If it's a Join whose [=object=] is my [=collaborators=] collection:
    - Verify the Accept is authorized with [=admin=] access
    - Verify this collaborator hasn't been enabled already
    - Enable this collaborator in DB
    - Send a Grant as described in [[#access-via-join]] step 2

- If it's an Add whose [=target=] is my [=teams=] collection,
    i.e. a Team Collaborator is being added to me and the process is
    initiated on *our* side:
    - Error/ignore, we aren't supposed get any Accept

- If it's an Add whose [=object=] is me and [=target=] points to a collection
    whose [=context=] is a [=Team=] that points back to the collection via
    its [=teamResources=] field, i.e. a Team Collaborator is being added to me
    and the process is initiated on *their* side:
    - Option 1: If I haven't yet seen the Team's Accept:
        Verify the Accept's [=actor=] is the Team
    - Option 2: If I saw Team's Acccept but not yet my collaborator's
        Accept: Verify the Accept is authorized with [=admin=] access
    - Otherwise, error/ignore, no Accept is needed
    - For option 1: Record Team's Accept in DB
    - For option 2: Record my collaborator's Accept in DB
    - Insert the Accept to my inbox & forward to my followers
    - For option 2, send an Accept, see [[#forming-team-resource-link-t]]
        step 4

- If it's an Add whose [=target=] is my [=context=] (i.e. projects) collection,
    i.e. I'm being added to a project and the process is initiated on *our*
    side:
    - Nothing to do, just ignore the Accept, waiting for the delegate-Grant
        from the Project

- If it's an Add whose [=object=] is me and [=target=] points to a collection
    whose [=context=] is a [=Project=] that points back to the collection via
    its [=components=] field, i.e. I'm being added to a project and the process
    is initiated on *their* side:
    - If I haven't yet seen the project's approval:
        - Verify the author is the project
        - Insert to my inbox & forward to my followers
    - If I saw project's approval, but not my collaborators' approval:
        - Verify the Accept is authorized with [=admin=] access
        - Insert to my inbox & forward to my followers
        - Publish an Accept, see [[#linking-projects-components-p]] step 4
    - If I already saw both approvals, respond with error

- If it's an Invite I've seen, whose [=object=] is me, and whose [=target=] is
    some [=Repository=]'s [=collaborators=] collection, and whose
    [=instrument=] is [=visit=]:
    - Verify Accept [=actor=] is the [=Repository=]
    - Send an [=Accept=], see [[#access-via-invite]] step 2

- If it's a [=Delete=] I've seen, of a [=Milestone=] that belongs to my
    [=trackerRoadmap=]:
    - Update my releases, unsetting their milestone if they're associated with
        the milestone being deleted

### Add ### {#rt-beh-add}

Meanings:

- A Team collaborator is being added to me
- I'm being added as a component of a Project
- A milestone is being assigned to one of my releases

Processes:

- [[#forming-team-resource-link]]
- [[#linking-projects-components]]
- [[#s2s-release-milestone-add]]

Behavior:

- If [=target=] is my [=teams=] collection, i.e. a Team Collaborator is being
    added to me and the process is initiated on *our* side:
    - Verify the [=object=] is a [=Team=]
    - Verify the Add is authorized with [=admin=] access
    - Verify it's not already an active team of mine
    - Insert the Add to my inbox & forward the Add to my followers
    - Publish an Accept, see [[#forming-team-resource-link-r]] step 2

- If [=object=] is me and [=target=] points to a collection
    whose [=context=] is a [=Team=] that points back to the collection via
    its [=teamResources=] field, i.e. a Team Collaborator is being added to me
    and the process is initiated on *their* side:
    - Verify it's not already an active team of mine
    - Insert to my inbox & forward to my followers

- If [=target=] is my [=context=] (i.e. projects) collection, i.e. I'm being
    added to a project and the process is initiated on *our* side:
    - Verify the Add is authorized with [=admin=] access
    - Verify the [=object=] is a [=Project=]
    - Verify I'm not yet a component of this Project
    - Insert to my inbox & forward to my followers
    - Publish an Accept, see [[#linking-projects-components-c]] step 2

- If [=object=] is me and [=target=] points to a collection whose [=context=]
    is a [=Project=] that points back to the collection via its [=components=]
    field, i.e. I'm being added to a project and the process is initiated on
    *their* side:
    - Verify I'm not already a component of this Project
    - Insert to my inbox & forward to my followers

- If [=target=] is the [=releaseMilestones=] collection of one of my releases:
    - Verify [=object=] is a [=Milestone=] that belongs to my [=trackerRoadmap=]
    - Verify the Add is authorized with [=write=] access
    - Insert the milestone to the release's [=releaseMilestones=] collection
    - Send an [=Accept=], see [[#s2s-release-milestone-add]] step 3

### Create ### {#rt-beh-create}

Meanings:

- Someone is commenting on a release of mine

Processes:

- [[#commenting]]

Behavior:

- Verify [=object=] is a [=Note=] whose [=context=] is a [=Release=] of mine
- Insert to inbox & forward to my followers

### Delete ### {#rt-beh-delete}

Meanings:

- Someone is deleting their comment on a release of mine
- Someone is asking to delete a release of mine
- Someone is asking to delete me
- A milestone is being deleted

Processes:

- [[#commenting]]
- [[#s2s-release-delete]]
- [[#deleting-resource-actors]]
- [[#s2s-milestone-delete]]

Behavior:

- If [=object=] is a [=Note=] whose [=context=] is a [=Release=] of mine:
    - Verify sender is the actual author of the Note
    - Insert to inbox & forward to my followers

- If [=object=] is a [=Release=] of mine:
    - Verify the Delete is authorized with [=write=] access
    - Delete the release from DB, such that GET requests for it now return 404
        or 410 status
    - Send an [=Accept=], see [[#s2s-release-delete]] step 3

- If [=object=] is me:
    - Verify the Delete is authorized with [=admin=] access
    - Send an [=Accept=], see [[#deleting-resource-actors]] step 2
    - Delete myself, such that further GET requests for me or any of my
        objects return 404 or 410 status

- If [=origin=] is my [=trackerRoadmap=] and [=object=] is a [=Milestone=]:
    - Just insert to my inbox & forward to my followers

### Edit ### {#rt-beh-edit}

Meanings:

- Someone is asking to edit my name and/or description
- Someone is deleting their comment on a release of mine

Processes:

- [[#s2s-edit-release-tracker]]
- [[#commenting]]

Behavior:

- If it's me:
    - Verify the Edit is authorized with [=admin=] access
    - Update me in DB from the Edit [=object=]'s fields: [=name=], [=summary=]

- If it's a [=Note=] whose [=context=] is a [=Release=] of mine:
    - Verify sender is the actual author of the Note
    - Insert to inbox & forward to my followers

### Follow ### {#rt-beh-follow}

Meanings:

- Someone is asking to follow me

Processes:

- [[#following]]

Behavior:

- Verify the [=object=] is me
- Verify the [=actor=] isn't already a follower of the [=object=]
- Verify the Follow is authorized with [=visit=] access (unless I'm considered
    a publicly visible resource and then the [=capability=] can be omitted)
- Insert [=actor=] to the list of followers of the [=object=]
- Send back an [=Accept=] whose [=object=] points to the [=Follow=]

### Grant ### {#rt-beh-grant}

Meanings:

- A Project-in-process is sending me the delegate-Grant
- A Team-Collaborator-in-process is sending me the delegate-Grant
- A [=Repository=] is sending me the direct-Grant

Processes:

- [[#linking-projects-components]]
- [[#forming-team-resource-link]]
- [[#s2s-release-tracker-repo-link]]

Behavior:

Check the Grant's [=fulfills=] field, verify it's an
[=Add=]/[=Invite=]/[=Join=] activity that I've seen:

- If it's a Project-in-process which is now sending me the delegate-Grant, and
    the previous steps in the process successfully happened:
    - Insert the Project to my list of Projects and enable in DB
    - Send the Project a start-Grant, see [[#linking-projects-components-c]]
        step 5 or [[#linking-projects-components-p]] step 6

- If it's a Team-Collaborator-in-process which is now sending me the
    delegate-Grant, and the previous steps in the process successfully
    happened:
    - Insert the Team to my list of Team Collaborators and enable in DB
    - Send the Team a start-Grant, see [[#deleg-step-tr]] step 1

- If it's a [=Repository=], in which I'm becoming a [=visit=] Collaborator, now
    sending me the direct-Grant:
    - Verify I'm not already tracking releases for this repository
    - Add the repository's URI to the values specified by [=tracksReleasesFor=]

### Invite ### {#rt-beh-invite}

Meanings:

- Someone is inviting someone else to be a collaborator in me
- Someone is inviting me to be a collaborator in a [=Repository=]

Processes:

- [[#access-via-invite]]
- [[#s2s-release-tracker-repo-link]]

Behavior:

- If [=target=] is my [=collaborators=] collection:
    - Verify the Invite is authorized with [=admin=] access
    - Verify the [=object=] isn't already a collaborator of me
    - Publish an [=Accept=] whose [=object=] points to the Invite

Note: The Accept in the last step does NOT indicate that a collaborator has
been added. It merely signals to the potential collaborator that the Invite is
authorized, and now they can decide whether to accept/reject/ignore it.

- If [=object=] is me and [=target=] is some [=Repository=]'s [=collaborators=]
    collection:
    - Verify [=instrument=] is [=visit=]
    - Just insert to my inbox & forward to my followers

### Join ### {#rt-beh-join}

Same as for [=Factory=], see [[#factory-beh-join]].

### Offer ### {#rt-beh-offer}

Meanings:

- Someone is submitting a new release

Processes:

- [[#s2s-release-create]]

Behavior:

- If [=target=] is me and [=object=] is a [=Release=]:
    - Verify the Offer is authorized with [=write=] access
    - Verify the [=Repository=] referred via the [=origin=]'s [=context=] is
        one of my [=tracksReleasesFor=] repos
    - Create new release in DB
    - Publish an [=Accept=] on the [=Offer=], see [[#s2s-release-create]] step
        3

### Reject ### {#rt-beh-reject}

Same as for [=TicketTracker=], see [[#tt-beh-reject]].

### Remove ### {#rt-beh-remove}

Meanings:

- One of my Collaborators is being removed
- One of my Team Collaborators is being removed
- I'm being removed from one of my Projects
- A milestone is being unassigned from one of my releases

Processes:

- [[#taking-away-access-using-remove-activities]]
- [[#removing-team-resource-link]]
- [[#linking-projects-components]]
- [[#s2s-release-milestone-remove]]

Behavior:

- If [=origin=] is my [=collaborators=] collection:
    - Verify the Remove is authorized with [=admin=] access
    - Verify the [=object=] is one of my collaborators
    - Remove the [=object=] actor from my list of collaborators and invalidate
        the direct-Grant I had sent them
    - Publish a [=Revoke=], see [[#taking-away-access-using-remove-activities]]
        step 2.4

- If [=origin=] is my [=context=] (i.e. projects) collection:
    - Verify the Remove is authorized with [=admin=] access
    - Verify the [=object=] is one of the projects in which I'm a component
    - Remove the [=object=] actor from my list of projects and invalidate the
        start-Grant I had sent it
    - Publish an [=Revoke=], see [[#unlinking-projects-components-c]] step 2

- If [=origin=] is my [=teams=] collection:
    - Verify the Remove is authorized with [=admin=] access
    - Verify the [=object=] is one of my Team Collaborators
    - Remove the [=object=] actor from my list of Team Collaborators and
        invalidate the start-Grant I had sent it
    - Publish an [=Accept=], see [[#removing-team-resource-link-r]] step 2

- If I'm the [=object=], being removed from the [=origin=] specifying the
    [=components=] collection of a Project of mine:
        - Just forward to my followers

- If I'm the [=object=], being removed from the [=origin=] specifying the
    [=teamResources=] collection of a Team Collaborator of mine:
        - Nothing to do, just waiting for the Team to send a Revoke on the
            delegate-Grant it had sent me

- If [=origin=] is the [=releaseMilestones=] collection of one of my releases:
    - Verify [=object=] is a milestone associated with that release
    - Verify the Remove is authorized with [=write=] access
    - Remove the milestone from the releases's [=releaseMilestones=] collection
    - Send an [=Accept=], see [[#s2s-release-milestone-remove]] step 3

### Revoke ### {#rt-beh-revoke}

Meanings:

- A direct-Grant given to me by the [=Repository=] is being revoked

Processes:

- [[#revoking-access]]

Behavior:

- Verify the [=actor=] is identical to the actor of the [=Grant=] referred by
    the Revoke's [=object=]
- Verify that Grant's [=target=] is me
- Optional: Verify none of my releases are from this repo OR remove those
    releases
- Remove this repository from my [=tracksReleasesFor=] field

### Undo ### {#rt-beh-undo}

Meanings:

- Someone is asking to unfollow me

Processes:

- [[#following]]

Behavior:

Check the Undo's [=object=].

- If it's a [=Follow=] whose [=object=] is me:
    - Verify the [=actor=] is a follower of the Follow's [=object=]
    - Remove [=actor=] from the list of followers of the Follow's [=object=]
    - Send back an [=Accept=] whose [=object=] points to the [=Undo=]

## Organization ## {#org-beh}

Vervis implementation: None currently

### Accept ### {#org-beh-accept}

Meanings:

- Someone is becoming a Collaborator in me
- Someone is becoming, or ceasing to be, a member in me
- A project/component is becoming, or ceasing to be, a listed asset of mine
- A Team Collaborator is being added to me

Processes:

- [[#access-via-invite]]
- [[#access-via-join]]
- [[#s2s-org-member]]
- [[#s2s-org-project]]
- [[#forming-team-resource-link]]

Behavior:

Check the Accept's [=object=] as follows.

- If it's an Invite whose [=target=] is my [=collaborators=] collection:
    - Verify Accept [=actor=] is the Invite's [=target=]
    - Verify this collaborator hasn't been enabled already
    - Enable this collaborator in DB
    - Insert the Accept to my inbox & forward to my followers
    - Send a Grant as described in [[#access-via-invite]] step 3

- If it's a Join whose [=object=] is my [=collaborators=] collection:
    - Verify the Accept is authorized with [=admin=] access
    - Verify this collaborator hasn't been enabled already
    - Enable this collaborator in DB
    - Insert the Accept to my inbox & forward to my followers
    - Send a Grant as described in [[#access-via-join]] step 2

- If it's an Add whose [=target=] is my [=orgAssets=] collection and [=object=]
    is some project/component actor:
    - Verify I haven't yet seen the project's Accept
    - If sender is the project:
        - Insert a [=Relationship=] to my [=orgAssets=] collection
        - Send an [=Accept=], see [[#s2s-org-member-add]] step 6
    - Otherwise:
        - Just forward to my followers

- If it's a Remove whose [=object=] is me and whose [=origin=] refers to the
    [=organizations=] of some project/component actor:
    - Verify the [=actor=] is the project
    - Remove the project from my [=orgAssets=] collection
    - Publish an [=Accept=], see [[#s2s-org-project-remove-p]] step 5

- If it's an [=Add=] whose [=target=] is my [=members=] collection:
    - Verify I've seen and authorized the Add
    - Verify I haven't seen the [=object=] person's Accept
    - Verify the sender is the [=Person=] referred by the [=object=]
    - Inserts a new [=Relationship=] object to my [=members=] collection
    - Send an [=Accept=], see [[#s2s-org-member-add]] step 4

- If it's an Add whose [=target=] is my [=teams=] collection,
    i.e. a Team Collaborator is being added to me and the process is
    initiated on *our* side:
    - Error/ignore, we aren't supposed get any Accept

- If it's an Add whose [=object=] is me and [=target=] points to a collection
    whose [=context=] is a [=Team=] that points back to the collection via
    its [=teamResources=] field, i.e. a Team Collaborator is being added to me
    and the process is initiated on *their* side:
    - Option 1: If I haven't yet seen the Team's Accept:
        Verify the Accept's [=actor=] is the Team
    - Option 2: If I saw Team's Acccept but not yet my collaborator's
        Accept: Verify the Accept is authorized with [=admin=] access
    - Otherwise, error/ignore, no Accept is needed
    - For option 1: Record Team's Accept in DB
    - For option 2: Record my collaborator's Accept in DB
    - Insert the Accept to my inbox & forward to my followers
    - For option 2, send an Accept, see [[#forming-team-resource-link-t]]
        step 4

### Add ### {#org-beh-add}

Meanings:

- I'm getting a new Team Collaborator
- A member is being added to me
- A project is being added to me

Processes:

- [[#forming-team-resource-link]]
- [[#s2s-org-member-add]]
- [[#s2s-org-project-add]]

Behavior:

- If [=target=] is my [=teams=] collection, i.e. a Team Collaborator is being
    added to me and the process is initiated on *our* side:
    - Verify the [=object=] is a [=Team=]
    - Verify the Add is authorized with [=admin=] access
    - Verify it's not already an active team of mine
    - Insert the Add to my inbox & forward the Add to my followers
    - Publish an Accept, see [[#forming-team-resource-link-r]] step 2

- If [=object=] is me and [=target=] points to a collection
    whose [=context=] is a [=Team=] that points back to the collection via
    its [=teamResources=] field, i.e. a Team Collaborator is being added to me
    and the process is initiated on *their* side:
    - Verify it's not already an active team of mine
    - Insert to my inbox & forward to my followers

- If [=target=] is my [=members=] collection:
    - Verify the [=Add=] is authorized with [=admin=] access
    - Send an [=Accept=], see [[#s2s-org-member-add]] step 2

- If [=target=] is my [=orgAssets=] collection:
    - Verify the [=object=] is a project or component
    - Verify the Add is authorized with [=admin=] access
    - Verify I'm not already listing this actor as an asset
    - Publish an Accept, see [[#s2s-org-project-add]] step 2

- If [=object=] is me and [=target=] is some project/component's
    [=organizations=] collection:
    - Verify I'm not already listing this actor as an asset
    - Insert to my inbox & forward to my followers

### Delete ### {#org-beh-delete}

Meanings:

- Someone is asking to delete me

Processes:

- [[#deleting-resource-actors]]

Behavior:

- If [=object=] is me:
    - Verify the Delete is authorized with [=admin=] access
    - Send an [=Accept=], see [[#deleting-resource-actors]] step 2
    - Delete myself, such that further GET requests for me or any of my
        objects return 404 or 410 status

### Edit ### {#org-beh-edit}

Meanings:

- Someone is asking to edit my name and/or description

Processes:

- [[#s2s-edit-team]]

Behavior:

Check the Edit's [=object=]'s [=id=].

- If it's me:
    - Verify the Edit is authorized with [=admin=] access
    - Update me in DB from the Edit [=object=]'s fields: [=name=], [=summary=]

### Follow ### {#org-beh-follow}

Meanings:

- Someone is asking to follow me

Processes:

- [[#following]]

Behavior:

- Verify the [=object=] is me
- Verify the [=actor=] isn't already a follower of the [=object=]
- Verify the Follow is authorized with [=visit=] access (unless I'm considered
    a publicly visible resource and then the [=capability=] can be omitted)
- Insert [=actor=] to the list of followers of the [=object=]
- Send back an [=Accept=] whose [=object=] points to the [=Follow=]

### Grant ### {#org-beh-grant}

Meanings:

- A Team-Collaborator-in-process is sending me the delegate-Grant

Processes:

- [[#forming-team-resource-link]]

Behavior:

Check the Grant's [=fulfills=] field, verify it's an
[=Add=]/[=Invite=]/[=Join=] activity that I've seen:

- If it's a Team-Collaborator-in-process which is now sending me the
    delegate-Grant, and the previous steps in the process successfully
    happened:
    - Insert the Team to my list of Team Collaborators and enable in DB
    - Send the Team a start-Grant, see [[#deleg-step-tr]] step 1

### Invite ### {#org-beh-invite}

Same as for [=Factory=], see [[#factory-beh-invite]].

### Join ### {#org-beh-join}

Same as for [=Factory=], see [[#factory-beh-join]].

### Reject ### {#org-beh-reject}

Same as for [=TicketTracker=], see [[#tt-beh-reject]].

### Remove ### {#org-beh-remove}

Meanings:

- One of my Collaborators is being removed
- One of my Team Collaborators is being removed
- One of my members is being removed
- One of my assets is being removed

Processes:

- [[#taking-away-access-using-remove-activities]]
- [[#removing-team-resource-link]]
- [[#s2s-org-member-remove-other]]
- [[#s2s-org-project]]

Behavior:

- If [=origin=] is my [=collaborators=] collection:
    - Verify the Remove is authorized with [=admin=] access
    - Verify the [=object=] is one of my collaborators
    - Remove the [=object=] actor from my list of collaborators and invalidate
        the direct-Grant I had sent them
    - Publish a [=Revoke=], see [[#taking-away-access-using-remove-activities]]
        step 2.4

- If [=origin=] is my [=teams=] collection:
    - Verify the Remove is authorized with [=admin=] access
    - Verify the [=object=] is one of my Team Collaborators
    - Remove the [=object=] actor from my list of Team Collaborators and
        invalidate the start-Grant I had sent it
    - Publish an [=Accept=], see [[#removing-team-resource-link-r]] step 2

- If I'm the [=object=], being removed from the [=origin=] specifying the
    [=teamResources=] collection of a Team Collaborator of mine:
        - Nothing to do, just waiting for the Team to send a Revoke on the
            delegate-Grant it had sent me

- If [=origin=] is my [=members=] collection:
    - Verify the Remove is authorized with [=admin=] access
    - Verify the [=object=] is one of my members
    - Remove the [=object=] person from my [=members=] collection
    - Send an [=Accept=], see [[#s2s-org-member-remove-other]] step 3

- If [=origin=] is my [=orgAssets=] collection:
    - Verify the Remove is authorized with [=admin=] access
    - Remove the [=object=] actor from my [=orgAssets=] collection
    - Send an [=Accept=], see [[#s2s-org-project-remove-o]] step 3

- If [=object=] is me and [=origin=] is the [=organizations=] collection of
    some project/component actor:
    - Just waiting for the project's Accept

### Undo ### {#org-beh-undo}

Meanings:

- Someone is asking to unfollow me

Processes:

- [[#following]]

Behavior:

Check the Undo's [=object=].

- If it's a [=Follow=] whose [=object=] is me:
    - Verify the [=actor=] is a follower of the Follow's [=object=]
    - Remove [=actor=] from the list of followers of the Follow's [=object=]
    - Send back an [=Accept=] whose [=object=] points to the [=Undo=]

<!-- -------------------------------------------------------------------------

        #####
 ####  #     #  ####
#    #       # #
#       #####   ####
#      #            #
#    # #       #    #
 ####  #######  ####

-------------------------------------------------------------------------- -->

# Client to Server Interactions

Issue: This section is about how a human or bot can interact with the system by
POSTing activities into the outbox of a Person (or Application/Service?) actor,
and managing notifications.  It's less urgent than Server-to-Server. fr33 is
using C2S in Vervis, and will be gradually working on this part.

ForgeFed uses Activities for client to server interactions, as described by
ActivityPub. A client will send objects (eg. a Ticket) wrapped in a Activity
(eg. Create) to an actor's outbox, and in turn the server will take care of
delivery.

## Follow Activity

The Follow activity is used to subscribe to the activities of a Repository.
The client MUST send a Follow activity to the Person's outbox. The server
in turn delivers the message to the destination inbox.

## Push Activity

The Push activity is used to notify followers when somebody has pushed changes
to a Repository.
The client MUST send a Push activity to the Repository's outbox. The server
in turn delivers the message to the Repository followers.

<!-- -------------------------------------------------------------------------

#####  #####  ######  ####  ###### #    #  ####    ##   ##### #  ####  #    #
#    # #    # #      #      #      ##   # #       #  #    #   # #    # ##   #
#    # #    # #####   ####  #####  # #  #  ####  #    #   #   # #    # # #  #
#####  #####  #           # #      #  # #      # ######   #   # #    # #  # #
#      #   #  #      #    # #      #   ## #    # #    #   #   # #    # #   ##
#      #    # ######  ####  ###### #    #  ####  #    #   #   #  ####  #    #

-------------------------------------------------------------------------- -->

# Actor and Resource Representation

Issue: This section is about representation of objects. It's possible that
actor representation will move into a separate section, as well as objects
specific to one actor type. And then this section will describe only
objects/resources used by multiple actor types, such as comments (person, issue
tracker, PR tracker) and tickets (used for both issues and PRs).

## Comment ## {#Comment}

To represent a comment, e.g. a comment on a ticket or a merge request, use the
ActivityPub [=Note=] type.

Properties:

<pre class=simpledef>
[=type=]:
    [=Note=]
[=attributedTo=]:
    The author of the comment
[=context=]:
    The topic of the discussion, e.g. a ticket or a merge request. It MUST be
    provided.
[=inReplyTo=]:
    The entity on which this comment replies. MUST be provided. If the comment
    is made directly on the discussion topic, then [=inReplyTo=] MUST be
    identical to [=context=]. Otherwise, set [=inReplyTo=] to the comment to
    which this comment replies. In that case both comments MUST have an
    identical [=context=].
[=content=], [=mediaType=], [=source=]:
    The comment text, in rendered form and in source form
</pre>

<div class=example>
<xmp highlight=json-ld>
{
    "@context": "https://www.w3.org/ns/activitystreams",
    "id": "https://forge.example/luke/comments/rD05r",
    "type": "Note",
    "attributedTo": "https://forge.example/luke",
    "context": "https://dev.example/aviva/game-of-life/merge-requests/19",
    "inReplyTo": "https://dev.example/aviva/comments/E9AGE",
    "mediaType": "text/html",
    "content": "<p>Thank you for the review! I'll submit a correction ASAP</p>",
    "source": {
        "mediaType": "text/markdown; variant=Commonmark",
        "content": "Thank you for the review! I'll submit a correction ASAP"
    },
    "published": "2019-11-06T20:49:05.604488Z"
}
</xmp>
</div>

## Team Membership

Properties:

<pre class=simpledef>
[=type=]:
    [=Relationship=]
[=subject=]:
    A [=Team=]
[=relationship=]:
    [=hasCollaborator=]
[=object=]:
    A [=Person=] who is a member of the `Team`
[=tag=]:
    The role that the member has in the team
</pre>

<div class=example>
<xmp highlight=json-ld>
{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://forgefed.org/ns"
    ],
    "id": "https://dev.example/teams/mobilizon-dev-team/members/ThmsicTj",
    "type": "Relationship",
    "subject": "https://dev.example/teams/mobilizon-dev-team",
    "relationship": "hasCollaborator",
    "object": "https://dev.example/people/celine",
    "tag": "develop"
}
</xmp>
</div>

## Team

Properties:

<pre class=simpledef>
[=type=]:
    [=Team=]
[=name=]:
    The user-given name of the team, e.g. "Gitea Development Team"
[=published=]:
    The time the team was created on the server
[=summary=]:
    A one-line user provided description of the project, as HTML, e.g.
    `"We are creating a code hosting platform"`
[=collaborators=]:
    [=Collection=] of the members of this team
[=subteams=]:
    Subteams of this team, i.e. teams whose members (and subteams) inherit the
    access that this team has been granted (to projects, repositories, etc.)
[=context=]:
    Parent [=Team=]s of this team, i.e. teams from which this team inherits
    access to projects, components and resources, e.g.  repositories, ticket
    trackers (and passes them to its [=collaborators=] and inherits them to its own
    [=subteams=])
[=oversees=]:
    The teams who give access-to-themselves to this team
[=overseenBy=]:
    The teams to whom this team gives access-to-itself
[=roleFilter=]:
    Optionally attenuate extension-Grants given to team members
</pre>

<div class=example>
<xmp highlight=json-ld>
{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://w3id.org/security/v2",
        "https://forgefed.org/ns"
    ],
    "id": "https://dev.example/teams/mobilizon-dev-team",
    "type": "Team",
    "name": "Mobilizon Development Team",
    "summary": "We're creating a federated tool for organizing events!",
    "members": {
        "type": "Collection",
        "totalItems": 3,
        "items": [
            { "type": "Relationship",
              "subject": "https://dev.example/teams/mobilizon-dev-team",
              "relationship": "hasMember",
              "object": "https://dev.example/people/alice",
              "tag": "admin"
            },
            { "type": "Relationship",
              "subject": "https://dev.example/teams/mobilizon-dev-team",
              "relationship": "hasMember",
              "object": "https://dev.example/people/bob",
              "tag": "maintain"
            },
            { "type": "Relationship",
              "subject": "https://dev.example/teams/mobilizon-dev-team",
              "relationship": "hasMember",
              "object": "https://dev.example/people/celine",
              "tag": "develop"
            }
        ]
    },
    "subteams": {
        "type": "Collection",
        "totalItems": 2,
        "items": [
            "https://dev.example/teams/mobilizon-backend-team",
            "https://dev.example/teams/mobilizon-frontend-team"
        ]
    },
    "context": "https://dev.example/teams/framasoft-developers",

    "publicKey": {
        "id": "https://dev.example/teams/mobilizon-dev-team#main-key",
        "owner": "https://dev.example/teams/mobilizon-dev-team",
        "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhki....."
    },
    "inbox": "https://dev.example/teams/mobilizon-dev-team/inbox",
    "outbox": "https://dev.example/teams/mobilizon-dev-team/outbox",
    "followers": "https://dev.example/teams/mobilizon-dev-team/followers"
}
</xmp>
</div>

## Project ## {#model-project}

Properties:

<pre class=simpledef>
[=type=]:
    [=Project=]
[=name=]:
    The user-given name of the project, e.g. "My cool project"
[=published=]:
    The time the project was created on the server
[=summary=]:
    A one-line user provided description of the project, as HTML, e.g.
    "`<p>A command-line tool that does cool things</p>`"
[=ticketsTrackedBy=]:
    The default ticket tracker to use when submitting a ticket to this project
    (this tracker MUST be listed under the project's [=components=])
[=subprojects=]:
    A [=Collection=] of the subprojects of this project
[=context=]:
    The parent [=Project=](s) to which this project belongs
[=likes=]:
    Collection of [=Like=] activities the project received
</pre>

<div class=example>
<xmp highlight=json-ld>
{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://forgefed.org/ns"
    ],
    "id": "https://dev.example/projects/wanderer",
    "type": "Project",

    "name": "Wanderer",
    "summary": "3D nature exploration game",
    "components": {
        "type": "Collection",
        "totalItems": 7,
        "items": [
            "https://dev.example/repos/opengl-vegetation",
            "https://dev.example/repos/opengl-vegetation/patch-tracker",
            "https://dev.example/repos/treesim",
            "https://dev.example/repos/treesim/patch-tracker",
            "https://dev.example/repos/wanderer",
            "https://dev.example/repos/wanderer/patch-tracker",
            "https://dev.example/issue-trackers/wanderer"
        ]
    },
    "subprojects": {
        "type": "Collection",
        "totalItems": 2,
        "items": [
            "https://dev.example/projects/nature-3d-models",
            "https://dev.example/projects/wanderer-fundraising"
        ]
    },
    "ticketsTrackedBy": "https://dev.example/issue-trackers/wanderer",

    "inbox": "https://dev.example/projects/wanderer/inbox",
    "outbox": "https://dev.example/projects/wanderer/outbox",
    "followers": "https://dev.example/projects/wanderer/followers",
    "likes": "https://dev.example/projects/wanderer/likes"
}
</xmp>
</div>

## Commit

To represent a named set of changes committed into a repository's history, use
the ForgeFed [=Commit=] type. Such a committed change set is called
e.g. a *commit* in Git, and a *patch* in Darcs.

Properties:

<pre class=simpledef>
[=type=]:
    [=Commit=]
[=context=]:
    The [=Repository=] that this commit belongs to
[=attributedTo=]:
    The commit author; if their actor URI is unknown, it MAY be their email
    address as a `mailto` URI
[=created=]:
    A value of type [=dateTime=] (i.e. an ISO 8601 datetime value) specifying
    the time at which the commit was written by its author
[=committedBy=]:
    The entity that committed the commit's changes into their local copy of the
    repo, before the commit was pushed; if their actor URI is unknown, it MAY
    be their email address as a `mailto` URI
[=committed=]:
    The time the commit was committed by its committer
[=hash=]:
    The hash identifying the commit, e.g. the commit SHA1 hash in Git; the
    patch info SHA1 hash in Darcs
[=summary=]:
    The commit's one-line title as HTML-escaped plain text; if the commit title
    and description are a single commit message string, then the title is the
    1st line of the commit message
[=description=]:
    A JSON object with a [=mediaType=] field and a [=content=] field, where
    `mediaType` SHOULD be "text/plain" and `content` is the commit's
    possibly-multi-line description; if the commit title and description are a
    single commit message string, then the description is everything after the
    1st line of the commit message (possibly with leading whitespace stripped)
</pre>

<div class=example>
<xmp highlight=json-ld>
{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://forgefed.org/ns"
    ],
    "id": "https://example.dev/alice/myrepo/commits/109ec9a09c7df7fec775d2ba0b9d466e5643ec8c",
    "type": "Commit",
    "context": "https://example.dev/alice/myrepo",
    "attributedTo": "https://example.dev/bob",
    "created": "2019-07-11T12:34:56Z",
    "committedBy": "https://example.dev/alice",
    "committed": "2019-07-26T23:45:01Z",
    "hash": "109ec9a09c7df7fec775d2ba0b9d466e5643ec8c",
    "summary": "Add an installation script, fixes issue #89",
    "description": {
        "mediaType": "text/plain",
        "content": "It's about time people can install it on their computers!"
    }
}
</xmp>
</div>

## Branch

To represent a repository branch, use the ForgeFed [=Branch=] type.
It can be a real built-in version control system branch (such as a Git branch)
or a copy of the repo used as a branch (e.g. in Darcs, which doesn't implement
branches, and the way to have branches is to keep multiple versions of the
repo).

Properties:

<pre class=simpledef>
[=type=]:
    [=Branch=]
[=context=]:
    The [=Repository=] that this branch belongs to
[=name=]:
    The user given name of the branch, e.g. "main"
[=ref=]:
    The unique identifier of the branch within the repo, e.g. "refs/heads/main"
[=team=]:
    If the branch has its own access/authority/visibility settings, this can be
    a [=Collection=] of the actors who have push/edit access to the branch
</pre>

<div class=example>
<xmp highlight=json-ld>
{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://forgefed.org/ns"
    ],
    "id": "https://example.dev/luke/myrepo/branches/master",
    "type": "Branch",
    "context": "https://example.dev/luke/myrepo",
    "name": "master",
    "ref": "refs/heads/master"
}
</xmp>
</div>

## Repository ## {#model-repo}

To represent a version control repository, use the ForgeFed
[=Repository=] type.

Properties:

<pre class=simpledef>
[=type=]:
    [=Repository=]
[=name=]:
    The user given name of the repository, e.g. "My cool repo"
[=cloneUri=]:
    The endpoint from which the content of the repository can be obtained via
    the native protocol (Git, Hg, etc.)
[=attributedTo=]:
    The actor(s) in charge of the repository, e.g. a person or an organization;
    if their actor URI is unknown, it MAY be their email address as a `mailto`
    URI
[=published=]:
    The time the repository was created on the server
[=summary=]:
    A one-line user provided description of the repository, as HTML, e.g.
    "`<p>A command-line tool that does cool things</p>`"
[=team=]:
    [=Collection=] of actors who have management/push access to the repository,
    or the subset of them who is available and wants to be
    contacted/notified/responsible on repo access related activities/requests
[=forks=]:
    [=OrderedCollection=] of repositories that are forks of this repository
[=ticketsTrackedBy=]:
    The ticket tracker that tracks tickets for this repository, this can be the
    repository itself if it manages its own tickets
[=sendPatchesTo=]:
    The actor that tracks patches for this repository, this can be the
    repository itself if it manages its own patches and merge requests. For
    example it may be some external tracker or service, or the user or team to
    whom the repository belongs.
[=context=]:
    The [=Project=](s) to which this repository belongs
[=likes=]:
    Collection of [=Like=] activities the repo received
</pre>

<div class=example>
<xmp highlight=json-ld>
{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://w3id.org/security/v1",
        "https://forgefed.org/ns"
    ],
    "id": "https://dev.example/aviva/treesim",
    "cloneUri": "https://dev.example/aviva/treesim.git",
    "type": "Repository",
    "publicKey": {
        "id": "https://dev.example/aviva/treesim#main-key",
        "owner": "https://dev.example/aviva/treesim",
        "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhki....."
    },
    "inbox": "https://dev.example/aviva/treesim/inbox",
    "outbox": "https://dev.example/aviva/treesim/outbox",
    "followers": "https://dev.example/aviva/treesim/followers",
    "likes": "https://dev.example/aviva/treesim/likes",
    "team": "https://dev.example/aviva/treesim/team",
    "ticketsTrackedBy": "https://dev.example/aviva/treesim",
    "sendPatchesTo": "https://dev.example/aviva/treesim",
    "name": "Tree Growth 3D Simulation",
    "attributedTo": "https://example.dev/bob",
    "summary": "<p>Tree growth 3D simulator for my nature exploration game</p>"
}
</xmp>
</div>

## Push ## {#Push}

To represent an event of [=Commit=]s being pushed to a
[=Repository=], use a ForgeFed [=Push=] activity.

Properties:

<pre class=simpledef>
[=type=]:
    [=Push=]
[=actor=]:
    The [=Repository=] to which the push was made, and that is publishing this
    Push activity
[=attributedTo=]:
    The entity (person, bot, etc.) that pushed the commits
[=target=]:
    The specific repo history tip onto which the commits were added, this is
    either a [=Branch=] (for VCSs that have branches) or a [=Repository=] (for
    VCSs that don't have branches, only a single history line). If it's a
    branch, it MUST be a branch belonging to the repository specified by
    [=actor=]. And if it's a repository, it MUST be identical to the one
    specified by [=actor=].
[=hashBefore=]:
    Repo/branch/tip hash before adding the new commits
[=hashAfter=]:
    Repo/branch/tip hash after adding the new commits
[=object=]:
    An [=OrderedCollection=] of the [=Commit=]s being pushed, in **reverse
    chronological order**. The [=items=] (or [=orderedItems=]) property of the
    collection MUST contain either the whole list of commits being pushed, or a
    prefix i.e. continuous subset from the beginning of the list (therefore the
    **latest** commits). [=earlyItems=] MAY be used for listing a suffix i.e.
    continuous subset from the end (therefore the **earliest** commits).
</pre>

<div class=example>
<xmp highlight=json-ld>
{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://forgefed.org/ns"
    ],
    "id": "https://dev.example/aviva/outbox/E26bE",
    "type": "Push",
    "actor": "https://dev.example/aviva",
    "to": [
        "https://dev.example/aviva/followers",
        "https://dev.example/aviva/game-of-life",
        "https://dev.example/aviva/game-of-life/team",
        "https://dev.example/aviva/game-of-life/followers"
    ],
    "context": "https://dev.example/aviva/game-of-life",
    "target": "https://dev.example/aviva/game-of-life/branches/master",
    "hashBefore": "017cbb00bc20d1cae85f46d638684898d095f0ae",
    "hashAfter": "be9f48a341c4bb5cd79ae7ab85fbf0c05d2837bb",
    "object": {
        "totalItems": 2,
        "type": "OrderedCollection",
        "orderedItems": [
            {
                "id": "https://dev.example/aviva/game-of-life/commits/be9f48a341c4bb5cd79ae7ab85fbf0c05d2837bb",
                "type": "Commit",
                "attributedTo": "https://dev.example/aviva",
                "context": "https://dev.example/aviva/game-of-life",
                "hash": "be9f48a341c4bb5cd79ae7ab85fbf0c05d2837bb",
                "created": "2019-12-02T16:07:32Z",
                "summary": "Add widget to alter simulation speed"
            },
            {
                "id": "https://dev.example/aviva/game-of-life/commits/fa37fe100a8b1e69933889c5bf3caf95cd3ae1e6",
                "type": "Commit",
                "attributedTo": "https://dev.example/aviva",
                "context": "https://dev.example/aviva/game-of-life",
                "hash": "fa37fe100a8b1e69933889c5bf3caf95cd3ae1e6",
                "created": "2019-12-02T15:51:52Z",
                "summary": "Set window title correctly, fixes issue #7"
            }
        ]
    }
}
</xmp>
</div>

## Ticket ## {#Ticket}

To represent a work item in a project, use the ForgeFed [=Ticket=]
type.

TODO decide on ticket categories/subtypes and update below

TODO decide on property for titles, update below

TODO properly document `history` or remove it from example

Properties:

<pre class=simpledef>
[=type=]:
    [=Ticket=]
[=context=]:
    The [=TicketTracker=] or [=PatchTracker=] to which this ticket belongs
[=attributedTo=]:
    The actor (person, bot, etc.) who submitted the ticket
[=summary=]:
    The ticket's one-line title, as HTML-escaped plain text
[=content=], [=mediaType=]:
    The ticket's (possibly multi-line) detailed description text, in rendered
    form
[=source=]:
    Source form of the ticket's description
[=published=]:
    The time the ticket submission was accepted (which may not be the same as
    the time the ticket was submitted)
[=followers=]:
    Collection of the followers of the ticket, actors who want to be notified
    on activity related to the ticket
[=team=]:
    Collection of project team members who have responsibility for work on this
    ticket and want to be notified on activities related to it
[=replies=]:
    Collection of direct comments made on the ticket (but not comments made *on
    other* comments on the ticket)
[=dependants=]:
    Collection of [=Ticket=]s which depend on this ticket
[=dependencies=]:
    Collection of [=Ticket=]s on which this ticket depends
[=isResolved=]:
    Whether the work on this ticket is done
[=resolvedBy=]:
    If the work on this ticket is done, who marked the ticket as resolved, or
    which activity did so
[=resolved=]:
    When the ticket has been marked as resolved
</pre>

There's an important distinction between these two kinds of tickets:

- Task:
    A work item which tracks some task to be done, in which the task and its
    results are described in text, but the work itself is done elsewhere.  This
    is often called "issue" in software project hosting platforms. Tasks are
    general-purpose work items that aren't specific to software development or
    software projects.
- Merge request:
    A work item that includes a proposal or request to apply some specific
    changes to a specific [=Repository=]. The work requested by the work item
    is to review and (decide whether to) merge the proposed patches to the
    repository. This kind of work item is often called "Pull Request" or "Merge
    Request" on software project hosting platforms.

### Task / Issue

A task is represented as a [[#Ticket]] as described above, with the
following additional requirements:

- [=context=] is the [=TicketTracker=] to which this task belongs
- There is no [=attachment=] of type [=Offer=] (but there may be attachment of other types)

<div class=example>
<xmp highlight=json-ld>
{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://forgefed.org/ns"
    ],
    "id": "https://dev.example/aviva/game-of-life/issues/107",
    "type": "Ticket",
    "context": "https://dev.example/aviva/game-of-life",
    "attributedTo": "https://forge.example/luke",
    "summary": "Window title is empty",
    "content": "<p>When I start the simulation, window title disappears suddenly</p>",
    "mediaType": "text/html",
    "source": {
        "mediaType": "text/markdown; variant=Commonmark",
        "content": "When I start the simulation, window title disappears suddenly",
    },
    "published": "2019-11-04T07:00:04.465807Z",
    "followers": "https://dev.example/aviva/game-of-life/issues/107/followers",
    "team": "https://dev.example/aviva/game-of-life/issues/107/team",
    "replies": "https://dev.example/aviva/game-of-life/issues/107/discussion",
    "history": "https://dev.example/aviva/game-of-life/issues/107/activity",
    "dependants": "https://dev.example/aviva/game-of-life/issues/107/rdeps",
    "dependencies": "https://dev.example/aviva/game-of-life/issues/107/deps",
    "isResolved": true,
    "resolvedBy": "https://code.example/martin",
    "resolved": "2020-02-07T06:45:03.281314Z"
}
</xmp>
</div>

### Merge Request / Pull Request

A merge request is represented as a [[#Ticket]] as described above, with
the following additional requirements:

- [=context=] is the [=PatchTracker=] to which this merge request belongs
- There is exactly one [=attachment=] of type [=Offer=], as described below
- There MAY be more [=attachment=]s, but they MUST NOT be of type [=Offer=]

In that special [=attachment=] of type [=Offer=]:

- [=type=] is [=Offer=]
- [=origin=] is the [=Repository=] or [=Branch=] from which the proposed changes are proposed to be merged into the target repository/branch
- [=target=] is the [=Repository=] or [=Branch=] into which the changes are proposed to be merged
- [=mrDiff=] is a [=Link=] whose [=href=] points to a unified diff file with
    the changes in the MR
- [=object=] is an [=OrderedCollection=] of [=Patch=]es in reverse
    chronological order, in which, in addition to standard [=OrderedCollection=]
    properties:
        - [=context=] is (the [=id=] of) the [[#Ticket]]
        - [=target=] is an object with a [=hash=] field specifying the commit
            ID of the last patch in the collection
        - [=previousVersions=] is a list of previous versions
            of the merge request's proposed changes, i.e. previous versions of this
            [=OrderedCollection=]; each of those uses
            [=currentVersion=] to point back to this latest
            version
- [=isWip=] may be specified
- [=individualApprovals=] is a collection of review approval statuses by people
- [=teamApprovals=] is a collection of review approval statuses by teams
- [=requiredApprovalsNeeded=] may be specified
- [=requiredApprovalsGiven=] may be specified

<div class=example>
<xmp highlight=json-ld>
{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://forgefed.org/ns"
    ],
    "id": "https://dev.example/aviva/game-of-life/pulls/825",
    "type": "Ticket",
    "context": "https://dev.example/aviva/game-of-life",
    "attributedTo": "https://forge.example/luke",
    "summary": "Fix the empty window title bug",
    "content": "<p>This fixes the bug making the title disappear</p>",
    "mediaType": "text/html",
    "source": {
        "mediaType": "text/markdown; variant=Commonmark",
        "content": "This fixes the bug making the title disappear",
    },
    "published": "2022-09-15T14:52:00.125987Z",
    "followers": "https://dev.example/aviva/game-of-life/pulls/825/followers",
    "replies": "https://dev.example/aviva/game-of-life/pulls/825/discussion",
    "isResolved": false,
    "attachment": {
        "type": "Offer",
        "origin": {
            "type": "Branch",
            "context": "https://forge.example/luke/game-of-life",
            "ref": "refs/heads/fix-title-bug"
        },
        "target": {
            "type": "Branch",
            "context": "https://dev.example/aviva/game-of-life",
            "ref": "refs/heads/main"
        },
        "object": {
            "id": "https://dev.example/aviva/game-of-life/pulls/825/versions/1",
            "type": "OrderedCollection",
            "totalItems": 1,
            "items": [
                {
                    "type": "Patch",
                    "attributedTo": "https://forge.example/luke",
                    "context": "https://dev.example/aviva/game-of-life/pulls/825/versions/1",
                    "mediaType": "application/x-git-patch",
                    "content": "From c9ae5f4ff4a330b6e1196ceb7db1665bd4c1..."
                }
            ],
            "context": "https://dev.example/aviva/game-of-life/pulls/825"
        }
    }
}
</xmp>
</div>

## Patch

<pre class=simpledef>
[=type=]:
    [=Patch=]
[=attributedTo=]:
    The [=Person=] who has written the patch
[=context=]:
    An [=OrderedCollection=] representing a sequence of patches, being
    submitted together as a proposed change to a certain [=Repository=], and
    this patch is an item in that collection
[=content=]:
    A description of the changes that this patch proposes, encoded in the
    format specified by [=mediaType=]
[=mediaType=]:
    A native patch format used by the Version Control System of the
    [=Repository=] to which the patch is proposed, and in which the [=content=]
    of this patch is encoded
</pre>

<div class=example>
<xmp highlight=json-ld>
{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://forgefed.org/ns"
    ],
    "id": "https://dev.example/aviva/game-of-life/pulls/825/versions/1/patches/1",
    "type": "Patch",
    "attributedTo": "https://forge.example/luke",
    "context": "https://dev.example/aviva/game-of-life/pulls/825/versions/1",
    "mediaType": "application/x-git-patch",
    "content": "From c9ae5f4ff4a330b6e1196ceb7db1665bd4c1..."
}
</xmp>
</div>

## Factory

Properties:

<pre class=simpledef>
[=type=]:
    [=Factory=]
[=name=]:
    The user-given name of the factory, e.g. "My default factory"
[=published=]:
    The time the project was created on the server
[=summary=]:
    A one-line user provided description of the factory, as HTML, e.g.
    "`<p>Allows local users to create public resources</p>`"
[=availableActorTypes=]:
    Zero or more resource actor types whose creatio is supported and enabled by
    this Factory.
</pre>

<div class=example>
<xmp highlight=json-ld>
{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://w3id.org/security/v2",
        "https://forgefed.org/ns"
    ],
    "id": "https://grape.fr33domlover.site/factories/YGQoZ",
    "type": "Factory",
    "name": "final-factory",
    "summary": ".",
    "availableActorTypes": [
        "TicketTracker",
        "Project",
        "Team",
        "Repository"
    ],

    "outbox": "https://grape.fr33domlover.site/factories/YGQoZ/outbox",
    "publicKey": [
        "https://grape.fr33domlover.site/akey1",
        "https://grape.fr33domlover.site/akey2"
    ],
    "collaborators": "https://grape.fr33domlover.site/factories/YGQoZ/collabs",
    "followers": "https://grape.fr33domlover.site/factories/YGQoZ/followers",
    "inbox": "https://grape.fr33domlover.site/factories/YGQoZ/inbox",
    "teams": "https://grape.fr33domlover.site/factories/YGQoZ/teams"
}
</xmp>
</div>

## EnumValue ## {#model-enum-value}

Properties:

<pre class=simpledef>
[=type=]:
    [=EnumValue=]
[=name=]:
    Plain-text name for the value.
[=summary=]:
    A one-line description, as HTML-escaped plain text.
[=published=]:
    The time the value was created on the server.
[=enumValueColor=]:
    RGB background color to use when displaying the value's name.
[=context=]:
    The [=Enum=] that this value belongs to.
</pre>

<div class=example>
<xmp highlight=json-ld>
TODO
</xmp>
</div>

## Enum ## {#model-enum}

Properties:

<pre class=simpledef>
[=type=]:
    [=Enum=]
[=name=]:
    Plain-text name for the enum.
[=enumIsOrdered=]:
    Boolean. Whether the enum value order matters or not.
[=enumValues=]:
    [=OrderedCollection=] of [=EnumValue=]s. If [=enumIsOrdered=] is false, the
    collection order is the order in which the values were created, first to
    last.
[=summary=]:
    A one-line description, as HTML-escaped plain text.
[=published=]:
    The time the enum was created on the server.
[=context=]:
    The [=Workflow=] that this enum belongs to.
</pre>

<div class=example>
<xmp highlight=json-ld>
TODO
</xmp>
</div>

## Field ## {#model-field}

Properties:

<pre class=simpledef>
[=type=]:
    [=Field=]
[=name=]:
    Plain-text name for the field.
[=summary=]:
    A one-line description, as HTML-escaped plain text.
[=prop:fieldType=]:
    The field's type.
[=instrument=]:
    If [=prop:fieldType=] is [=fieldTypeEnum=], specifies the specific [=Enum=].
[=fieldColor=]:
    RGB background color to use when displaying the field's name, and possibly
    its value as well.
[=published=]:
    The time the field was created on the server.
[=context=]:
    The [=Workflow=] that this field belongs to.
</pre>

<div class=example>
<xmp highlight=json-ld>
TODO
</xmp>
</div>

## Milestone ## {#model-milestone}

Properties:

<pre class=simpledef>
[=type=]:
    [=Milestone=]
[=name=]:
    Plain-text title.
[=content=], [=mediaType=]:
    The milestone's (possibly multi-line) detailed description text,
    in rendered form
[=source=]:
    Source form of the milestone's description
[=published=]:
    The time the milestone was created on the server.
[=startTime=]:
    The time that work on the milestone is expected to begin.
[=endTime=]:
    The time that work on the milestone is planned to end.
[=context=]:
    The [=Roadmap=] that this field belongs to.
[=milestoneTickets=]:
    [=Collection=] of [=Ticket=]s that belong to this milestone.
[=milestoneReleases=]:
    [=Collection=] of [=Release=]s associated with this milestone.
[=isResolved=]:
    Whether the milestone is closed.
</pre>

<div class=example>
<xmp highlight=json-ld>
TODO
</xmp>
</div>

## Release ## {#model-release}

Properties:

<pre class=simpledef>
[=type=]:
    [=Release=]
[=name=]:
    The tag this release was generated from.
[=summary=]:
    Title, as HTML-escaped plain text.
[=content=], [=mediaType=]:
    Release notes, in rendered form
[=source=]:
    Source form of release notes
[=published=]:
    The time the release was created on the server.
[=context=]:
    The [=ReleaseTracker=] that this release belongs to.
[=origin=]:
    The [=Commit=] this release was generated from - MUST specify at least
    [=hash=] (commit hash) and [=context=] (the [=Repository=])
[=attachment=]:
    [=Link=] objects describing release assets, each specifying [=href=],
    [=mediaType=] and [=name=]
[=isPreRelease=]:
    Whether this is a pre-release.
[=releaseMilestones=]:
    [=Collection=] of [=Milestone=]s this release is associated with
[=replies=]:
    Discussion on the release.
</pre>

<div class=example>
<xmp highlight=json-ld>
TODO
</xmp>
</div>

## Review ## {#model-review}

The representation of a review involves several main pieces:

1. The set of approvals that each Merge Request has
2. The reviews that reviewers have given
3. The individual threads of discussion on specific lines of code within a
    review
4. The tree of comment in those threads
5. The piece of code under which the thread begins
6. Code suggestions

### Code Quote ### {#model-review-code}

The piece of code is represented as follows:

<pre class=simpledef>
[=type=]:
    [=CodeQuote=]
[=cqFile=]:
    Path of the file being commented, e.g. "src/main.c"
[=cqHunk=]:
    A diff hunk that contains the line(s) being commented, and possibly context
    lines before and/or after
[=cqLine=], [=cqSide=]:
    The line being commented
[=cqStart=], [=cqEnd=], [=cqStartSide=], [=cqEndSide=]:
    The line range being commented
[=cqOutdated=]:
    Whether these lines have been changed by a later version of the MR
</pre>

There are 3 options for the subject of a CodeQuote:

1. If it refers to a file, only [=cqFile=] is specified
2. If it refers to a line, [=cqLine=] and [=cqSide=] are also specified
3. If it refers to a range of lines, then [=cqStart=], [=cqEnd=],
    [=cqStartSide=] and [=cqEndSide=] are specified instead

<div class=example>
<xmp highlight=json-ld>
TODO
</xmp>
</div>

### Code Suggestion ### {#model-review-suggestion}

Properties:

<pre class=simpledef>
[=type=]:
    [=Suggestion=]
[=attributedTo=]:
    The author of the suggestion
[=context=]:
    The [=Note=] this suggestion belongs to
[=source=]:
    Raw text of the code suggestion
[=content=], [=mediaType=]:
    HTML rendered form of the suggestion, as diff view (+, -, line numbers)
[=name=]:
    Description of this suggestion, as plain text
</pre>

<div class=example>
<xmp highlight=json-ld>
TODO
</xmp>
</div>

### Comment ### {#model-review-comment}

Every comment in the tree of comments within a review, including both the
review comments by the reviewer and the replies on them, is represented as a
regular [=Note=] (much like issue comments), except its [=attachment=]s may
contain code suggestions.

<pre class=simpledef>
[=type=]:
    [=Note=]
[=attributedTo=]:
    The author of the review comment
[=context=]:
    The [=ReviewThread=] this comment belongs to
[=inReplyTo=]:
    The [=Note=] being replied to, or the [=context=] [=ReviewThread=] itself
[=content=], [=mediaType=], [=source=]:
    The comment text, in rendered form and in source form
[=attachment=]:
    Zero or more [=Suggestion=]s
</pre>

<div class=example>
<xmp highlight=json-ld>
TODO
</xmp>
</div>

### Thread ### {#model-review-thread}

While suggestions and comments are hosted by their authors, threads are hosted
by the [=PatchTracker=]. However, the comment under the code quote, i.e. the
top comment of the thread, is hosted by its author, just like any other
comment.

<pre class=simpledef>
[=type=]:
    [=ReviewThread=]
[=context=]:
    The [=Review=] this comment belongs to
[=target=]:
    The [=CodeQuote=], i.e. lines of code being commented on
[=isResolved=], [=resolvedBy=], [=resolved=]:
    Whether the thread is resolved
[=object=]:
    The [=Note=] that is the top comment of this thread
</pre>

<div class=example>
<xmp highlight=json-ld>
TODO
</xmp>
</div>

### Review ### {#model-review-review}

A review is essentially a combination of:

1. A set of [=ReviewThread=]s
2. A summary comment
3. A status: Approve / neutral / request changes

<pre class=simpledef>
[=type=]:
    [=Review=]
[=context=]:
    The [=Ticket=] (i.e. Merge Request) to which this review belongs
[=attachment=]:
    The [=Note=] that is the summary comment of the review
[=verdict=]:
    The level of approval that the review gives on the merge request
[=reviewIsBinding=]:
    Whether the review is made by someone with [=write=] or higher access to
    the [=PatchTracker=], i.e. someone with access to apply a merge request
[=object=]:
    URI of an [=OrderedCollection=] of [=Patch=]es, representing the version of
    the merge request, on which the review was made. That collection's
    [=target=] can be used to determine the commit hash, i.e. the repository
    tree state on which the review was made.
[=reviewThreads=]:
    Zero or more [=ReviewThread=]s
</pre>

<div class=example>
<xmp highlight=json-ld>
TODO
</xmp>
</div>

### Approval ### {#model-review-approval}

Every merge request has a set of review statuses. This includes the last review
given by each reviewer, as well as review requests still awaiting review, and
also reviews that have been dismissed.

Since reviews can be requested and given by both individuals and teams, there
are two collections of these review statuses. But both are represented as
[=Approval=] objects.

For an individual:

<pre class=simpledef>
[=type=]:
    [=Approval=]
[=attributedTo=]:
    The [=Person=] to whom this approval relates
[=published=]:
    The time this approval has been created
[=approvalStatus=]:
    The [=ReviewStatus=] of this approval
[=reviewIsBinding=]:
    Whether the review is made by someone with [=write=] or higher access to
    the [=PatchTracker=], i.e. someone with access to apply a merge request
[=reviewIsRequested=]:
    Whether a review from this person was specifically personally
    requested/invited, or was is given without a personal request
[=reviewIsByCodeOwner=]:
    Whether this review/request is here because (perhaps among other reasons)
    this person is a Code Owner of some of the files being changed by the merge
    request
[=reviewCountsInRule=]:
    Whether this approval count towards required amount of approvals under some
    rule
[=newReviewInProgress=]:
    Whether the person has indicated they're in the process of making a new
    review
[=attachment=]:
    URI of the last [=Review=] by this person
</pre>

<div class=example>
<xmp highlight=json-ld>
TODO
</xmp>
</div>

For a team:

<pre class=simpledef>
[=type=]:
    [=Approval=]
[=attributedTo=]:
    The [=Team=] to whom this approval relates
[=published=]:
    The time this approval has been created
[=approvalStatus=]:
    The [=ReviewStatus=] of this approval - note that values [=remark=] and
    [=revise=] MUST NOT be used
[=reviewIsRequested=]:
    Whether a review from this team was specifically personally
    requested/invited, or was is given without a personal request
[=reviewIsByCodeOwner=]:
    Whether this review/request is here because (perhaps among other reasons)
    this team is a Code Owner of some of the files being changed by the merge
    request
[=reviewCountsInRule=]:
    Whether this approval count towards required amount of approvals under some
    rule
[=attachment=]:
    Zero or more [=Approval=]s by individuals, whose approval is in the name of
    the team
</pre>

<div class=example>
<xmp highlight=json-ld>
TODO
</xmp>
</div>

## Organization Membership

Properties:

<pre class=simpledef>
[=type=]:
    [=Relationship=]
[=subject=]:
    An [=Organization=]
[=relationship=]:
    [=hasMember=]
[=object=]:
    A [=Person=] who is a member of the `Organization`
</pre>

<div class=example>
<xmp highlight=json-ld>
TODO
</xmp>
</div>

<!-- -------------------------------------------------------------------------

  ##    ####   ####  ######  ####   ####
 #  #  #    # #    # #      #      #
#    # #      #      #####   ####   ####
###### #      #      #           #      #
#    # #    # #    # #      #    # #    #
#    #  ####   ####  ######  ####   ####


 ####   ####  #    # ##### #####   ####  #
#    # #    # ##   #   #   #    # #    # #
#      #    # # #  #   #   #    # #    # #
#      #    # #  # #   #   #####  #    # #
#    # #    # #   ##   #   #   #  #    # #
 ####   ####  #    #   #   #    #  ####  ######

-------------------------------------------------------------------------- -->

# Access Control

## Giving Access

### Invite ### {#Invite}

To offer some actor access to a shared resource (such as a repository or a
ticket tracker), use an ActivityPub [=Invite=] activity.

Properties:

<pre class=simpledef>
[=type=]:
    [=Invite=]
[=actor=]:
    The entity (person, bot, etc.) that is offering access
[=instrument=]:
    A [=Role=] specifying which operations on the resource are being allowed
[=target=]:
    The resource, access to which is being given (for example, a repository)
[=object=]:
    The actor who is being gives access to the resource
[=capability=]:
    A previously published `Grant`, giving the `actor` permission to invite
    more actors to access the resource
</pre>

<div class=example>
<xmp highlight=json-ld>
{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://forgefed.org/ns"
    ],
    "id": "https://dev.example/aviva/outbox/B47d3",
    "type": "Invite",
    "actor": "https://dev.example/aviva",
    "to": [
        "https://dev.example/aviva/followers",
        "https://coding.community/repos/game-of-life",
        "https://coding.community/repos/game-of-life/followers",
        "https://software.site/bob",
        "https://software.site/bob/followers"
    ],
    "instrument": "maintain",
    "target": "https://coding.community/repos/game-of-life",
    "object": "https://software.site/bob",
    "capability": "https://coding.community/repos/game-of-life/outbox/2c53A"
}
</xmp>
</div>

### Join ### {#Join}

To request access to a shared resource, use an ActivityPub [=Join=] activity.

Properties:

<pre class=simpledef>
[=type=]:
    [=Join=]
[=actor=]:
    The entity (person, bot, etc.) that is requesting access
[=instrument=]:
    A [=Role=] specifying which operations on the resource are being requested
[=object=]:
    The resource, access to which is being given (for example, a repository)
[=capability=]:
    *(optional)* A previously published `Grant`, giving the `actor` permission
    to gain access to the resource without the approval of another actor. If
    `capability` isn't provided, the resource won't grant access before someone
    with adequate access approves the Join request.
</pre>

<div class=example>
<xmp highlight=json-ld>
{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://forgefed.org/ns"
    ],
    "id": "https://software.site/bob/outbox/c97E3",
    "type": "Join",
    "actor": "https://software.site/bob",
    "to": [
        "https://coding.community/repos/game-of-life",
        "https://coding.community/repos/game-of-life/followers",
        "https://software.site/bob/followers"
    ],
    "instrument": "maintain",
    "object": "https://coding.community/repos/game-of-life",
    "capability": "https://coding.community/repos/game-of-life/outbox/d38Fa"
}
</xmp>
</div>

### Grant ### {#Grant}

To give some actor access to a shared resource, use a ForgeFed
[=Grant=] activity.

Properties:

<pre class=simpledef>
[=type=]:
    [=Grant=]
[=actor=]:
    The entity (person, bot, etc.) that is giving access
[=object=]:
    A [=Role=] specifying which operations on the resource are being allowed
[=context=]:
    The resource, access to which is being given (for example, a repository)
[=target=]:
    The actor who is being gives access to the resource
[=fulfills=]:
    The activity that triggered the sending of the `Grant`, such as a related
    `Invite` (another example&#x3a; if Alice [=Create=]s a new repository, the
    repository may automatically send back a [=Grant=] giving Alice admin
    access, and this Grant's `fulfills` refers to the [=Create=] that Alice
    sent)
[=result=]:
    A URI that can be used later for verifying that the given access is still
    approved, thus allowing the actor granting the access to revoke it.
    Alternatively, a JSON object where [=id=] is the URI and [=duration=] MAY
    be specified to allow to skip the revocation check if the duration time
    hasn't yet passed since the last check. If [=duration=] is specified, it
    MUST be positive, and specify only an integral number of seconds that is
    less than `2^63`, and no other component.
[=allows=]:
    Modes of invocation and/or delegation that this `Grant` is meant to be used
    for
[=delegates=]:
    If this `Grant` is a delegation, i.e. it is passing on some access that it
    has received, `delegates` specifies the parent `Grant` that it has received
    and now passing on
[=startTime=]:
    *(optional)* The time at which the Grant becomes valid
[=endTime=]:
    *(recommended)* The time at which the Grant expires
</pre>

<div class=example>
<xmp highlight=json-ld>
{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://forgefed.org/ns"
    ],
    "id": "https://coding.community/repos/game-of-life/outbox/9fA8c",
    "type": "Grant",
    "actor": "https://coding.community/repos/game-of-life",
    "to": [
        "https://dev.example/aviva",
        "https://dev.example/aviva/followers",
        "https://coding.community/repos/game-of-life/followers",
        "https://software.site/bob",
        "https://software.site/bob/followers"
    ],
    "object": "maintain",
    "context": "https://coding.community/repos/game-of-life",
    "target": "https://software.site/bob",
    "fulfills": "https://dev.example/aviva/outbox/B47d3",
    "allows": "invoke",
    "endTime": "2023-12-31T23:00:00-08:00"
}
</xmp>
</div>

## Canceling Access

### Remove ### {#Remove}

To disable an actor's membership in a shared resource, invalidating their
access to it, use an ActivityPub [=Remove=] activity.

Properties:

<pre class=simpledef>
[=type=]:
    [=Remove=]
[=actor=]:
    The actor (person, bot, etc.) that is disabling access disabled
[=object=]:
    The actor whose access to the resource is being taken away
[=origin=]:
    The resource, access to which is being taken away (for example, a
    repository)
[=capability=]:
    A previously published `Grant`, giving the `actor` permission to disable
    the [=object=] actor's access to the resource
</pre>

<div class=example>
<xmp highlight=json-ld>
{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://forgefed.org/ns"
    ],
    "id": "https://dev.example/aviva/outbox/F941b",
    "type": "Remove",
    "actor": "https://dev.example/aviva",
    "to": [
        "https://dev.example/aviva/followers",
        "https://coding.community/repos/game-of-life",
        "https://coding.community/repos/game-of-life/followers",
        "https://software.site/bob",
        "https://software.site/bob/followers"
    ],
    "origin": "https://coding.community/repos/game-of-life",
    "object": "https://software.site/bob",
    "capability": "https://coding.community/repos/game-of-life/outbox/2c53A"
}
</xmp>
</div>

### Leave ### {#Leave}

To withdraw your consent for membership in a shared resource, invalidating
your access to it, use an ActivityPub [=Leave=] activity.

Properties:

<pre class=simpledef>
[=type=]:
    [=Leave=]
[=actor=]:
    The actor (person, bot, etc.) that is requesting to disable their own
    access
[=object=]:
    The resource, access to which is being disabled (for example, a repository)
</pre>

<div class=example>
<xmp highlight=json-ld>
{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://forgefed.org/ns"
    ],
    "id": "https://software.site/bob/outbox/d08F4",
    "type": "Leave",
    "actor": "https://software.site/bob",
    "to": [
        "https://coding.community/repos/game-of-life",
        "https://coding.community/repos/game-of-life/followers",
        "https://software.site/bob/followers"
    ],
    "object": "https://coding.community/repos/game-of-life"
}
</xmp>
</div>

### Revoke ### {#Revoke}

Another activity that can be used for disabling access is [=Revoke=].
While [[#Remove]] and [[#Leave]] are meant for undoing the effects
of [[#Invite]] and [[#Join]], `Revoke` is provided as an opposite of
[[#Grant]]. See the Behavior specification for more information about the
usage of these different activity types in revocation of access to shared
resources.

Properties:

<pre class=simpledef>
[=type=]:
    [=Revoke=]
[=actor=]:
    The actor (person, bot, etc.) that is revoking access
[=fulfills=]:
    An activity that triggered the sending of the `Revoke`, such as a related
    `Remove` or `Leave`
[=object=]:
    specific [[#Grant]] activities being undone, i.e. the access that they
    granted is now disabled and it cannot be used anymore as the [=capability=]
    of activities. Each Grant may either be mentioned by its [=id=] URI, or be
    included as a full object that includes an [[fep-8b32|integrity proof]].
</pre>

<div class=example>
<xmp highlight=json-ld>
{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://forgefed.org/ns"
    ],
    "id": "https://coding.community/repos/game-of-life/outbox/1C0e2",
    "type": "Revoke",
    "actor": "https://coding.community/repos/game-of-life",
    "to": [
        "https://coding.community/repos/game-of-life/followers",
        "https://software.site/bob",
        "https://software.site/bob/followers"
    ],
    "object": "https://coding.community/repos/game-of-life/outbox/9fA8c"
}
</xmp>
</div>

### Undo a Grant ### {#undo-grant}

The Behavior spec describes flows in which the [[#Revoke]] activity is
used by resources (more accurately, by the actors managing them) to announce
that they're disabling [[#Grant]]s that they previously sent. To allow for
a clear distinction, another activity is provided here, for *other* actors to
*request* the revocation of specific [[#Grant]]s: The ActivityPub [=Undo=]
activity.

It's likely that `Grant`s would exist behind-the-scenes in applications, and
human actors would then use activities such as `Remove` and `Leave` for
disabling access. But the ability to disable specific `Grant`s may be required
for ensuring and maintaining system security, therefore `Undo` is provided here
as well.

Properties:

<pre class=simpledef>
[=type=]:
    [=Undo=]
[=actor=]:
    The actor (person, bot, etc.) that is revoking access
[=object=]:
    specific [[#Grant]] activities being undone, i.e. the access that they
    granted is now disabled and it cannot be used anymore as the [=capability=]
    of activities
[=capability=]:
    A previously published `Grant`, giving the `actor` permission to disable
    the [=object=] actor's access to the resource
</pre>

<!-- -------------------------------------------------------------------------

#    #  ####   ####    ##   #####  #    # #        ##   #####  #   #
#    # #    # #    #  #  #  #    # #    # #       #  #  #    #  # #
#    # #    # #      #    # #####  #    # #      #    # #    #   #
#    # #    # #      ###### #    # #    # #      ###### #####    #
 #  #  #    # #    # #    # #    # #    # #      #    # #   #    #
  ##    ####   ####  #    # #####   ####  ###### #    # #    #   #

-------------------------------------------------------------------------- -->

# Vocabulary # {#vocab}

## Types

The base URI of all ForgeFed terms is `https://forgefed.org/ns#`.
The ForgeFed vocabulary has a JSON-LD context whose URI is
`https://forgefed.org/ns`. Implementers MUST either include the
ActivityPub and ForgeFed contexts in their object definitions, or other
contexts that would result with the ActivityPub and ForgeFed terms being
assigned they correct full URIs. Implementers MAY include additional contexts
and terms as appropriate.

A typical `@context` of a ForgeFed object may look like this:

<div class=example>
<xmp highlight=json-ld>
"@context": [
    "https://www.w3.org/ns/activitystreams",
    "https://forgefed.org/ns"
]
</xmp>
</div>

### Activities

#### General

<pre class=simpledef>
Name: <dfn dfn export>Edit</dfn>
URI: https://forgefed.org/ns#Edit
Extends: [=Activity=]
Description:
    Indicates that [=actor=] is editing or asking to edit the object specified
    by [=object=]. In that object, [=id=] MUST be specified, and every other
    field is a field being set to a new value.
</pre>

<div class=example>
<xmp highlight=json-ld>
{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://forgefed.org/ns"
    ],
    "id": "http://dev.example/people/JABld/outbox/AZ01d",
    "actor": "http://dev.example/people/JABld",
    "type": "Edit",
    "object": {
        "id": "http://dev.example/people/JABld/messages/PAQlD",
        "source": "Hello world",
        "content": "<p>Hello world</p>"
    },
    "to": [
        "http://dev.example/decks/ld6zd",
        "http://dev.example/people/JABld/followers",
        "http://dev.example/decks/ld6zd/followers",
        "http://dev.example/decks/ld6zd/tickets/kd9ed/followers"
    ]
}
</xmp>
</div>

#### Related to access control

<pre class=simpledef>
Name: <dfn dfn export>Grant</dfn>
URI: https://forgefed.org/ns#Grant
Extends: [=Activity=]
Description:
    Indicates that [=target=] is being given (by the [=actor=]) access to a
    resource specified by [=context=] under the role/permission specified by
    [=object=].
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=7>
{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://forgefed.org/ns"
    ],
    "id": "https://example.dev/aviva/outbox/reBGo",
    "type": "Grant",
    "actor": "https://example.dev/aviva",
    "to": [
        "https://example.dev/aviva/followers",
        "https://example.dev/aviva/myproject",
        "https://example.dev/aviva/myproject/followers",
        "https://example.dev/bob",
        "https://example.dev/bob/followers"
    ],
    "object": "write",
    "context": "https://example.dev/aviva/myproject",
    "target": "https://example.dev/bob"
}
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>Revoke</dfn>
URI: https://forgefed.org/ns#Revoke
Extends: [=Activity=]
Description:
    Indicates that the [=actor=] is canceling [=target=]'s access to a resource
    specified by [=context=] under the role specified by [=instrument=], making
    the [=Grant=] activities specified by [=object=] unusable anymore in other
    activities' [=capability=] field.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=7>
{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://forgefed.org/ns"
    ],
    "id": "https://example.dev/myproject/outbox/nlTxb",
    "type": "Revoke",
    "actor": "https://example.dev/myproject",
    "to": [
        "https://example.dev/myproject/followers",
        "https://example.dev/users/aviva"
    ],
    "object": "https://example.dev/myproject/outbox/reBGo",
    "instrument": "https://example.dev/roles/developer",
    "context": "https://example.dev/myproject",
    "target": "https://example.dev/users/aviva"
}
</xmp>
</div>

#### Related to repositories

<pre class=simpledef>
Name: <dfn dfn export>Push</dfn>
URI: https://forgefed.org/ns#Push
Extends: [=Activity=]
Description:
    Indicates that new content has been pushed to the [=Repository=].
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=7>
{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://forgefed.org/ns"
    ],
    "id": "https://example.dev/aviva/myproject/outbox/reBGo",
    "type": "Push",
    "actor": "https://example.dev/aviva/myproject",
    "attributedTo": "https://example.dev/aviva",
    "to": [
        "https://example.dev/aviva",
        "https://example.dev/aviva/followers",
        "https://example.dev/aviva/myproject/followers"
    ],
    "summary": "<p>Aviva pushed a commit to myproject</p>",
    "object": {
        "type": "OrderedCollection",
        "totalItems": 1,
        "items": [
            {
                "id": "https://example.dev/aviva/myproject/commits/d96596230322716bd6f87a232a648ca9822a1c20",
                "type": "Commit",
                "attributedTo": "https://example.dev/aviva",
                "context": "https://example.dev/aviva/myproject",
                "hash": "d96596230322716bd6f87a232a648ca9822a1c20",
                "created": "2019-11-03T13:43:59Z",
                "summary": "Provide hints in sign-up form fields",
            }
        ]
    },
    "target": "https://example.dev/aviva/myproject/branches/master"
}
</xmp>
</div>

#### Related to ticket tracking

<pre class=simpledef>
Name: <dfn dfn export>Assign</dfn>
URI: https://forgefed.org/ns#Assign
Extends: [=Activity=]
Description:
    Indicates that a [=Ticket=] is being assigned to a [=Person=] or [=Team=].
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=7>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>Resolve</dfn>
URI: https://forgefed.org/ns#Resolve
Extends: [=Activity=]
Description:
    Indicates that a [=Ticket=] is being closed.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=7>
{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://forgefed.org/ns"
    ],
    "id": "https://grape.fr33domlover.site/people/WZpnG/outbox/GQvnR",
    "type": "Resolve",
    "actor": "https://grape.fr33domlover.site/people/WZpnG",
    "object": "https://fig.fr33domlover.site/decks/W058b/tickets/3bPVn",

    "capability": "https://grape.fr33domlover.site/groups/OZLdZ/outbox/ZLdDZ",

    "to": [
        "https://grape.fr33domlover.site/people/WZpnG/followers",
        "https://fig.fr33domlover.site/decks/W058b",
        "https://fig.fr33domlover.site/decks/W058b/followers",
        "https://fig.fr33domlover.site/decks/W058b/tickets/3bPVn/followers"
    ],

    "proof": {
        "created": "2024-07-09T07:02:45.038760903Z",
        "cryptosuite": "jcs-eddsa-2022",
        "proofPurpose": "assertionMethod",
        "proofValue": "z4yiaWtjPeUjBvoXpHffjguVd8uHsSfYr4zxxFJbZwPVLtRNCk3uMKtnC3nRZ8vRM3h36B6VLrFagVerxcymhj2NP",
        "type": "DataIntegrityProof",
        "verificationMethod": "https://grape.fr33domlover.site/akey1"
    }
}
</xmp>
</div>

#### Related to merge requests

<pre class=simpledef>
Name: <dfn dfn export>Apply</dfn>
URI: https://forgefed.org/ns#Apply
Extends: [=Activity=]
Description:
    Indicates a request to apply a Merge Request to a [=Repository=].
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=7>
{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://forgefed.org/ns"
    ],
    "id": "https://fig.fr33domlover.site/people/qn870/outbox/ZnqL0",
    "type": "Apply",
    "actor": "https://fig.fr33domlover.site/people/qn870",
    "object": "https://fig.fr33domlover.site/looms/9nOkn/cloths/mbWob/bundles/mbWob",
    "target": {
        "type": "Branch",
        "context": "https://fig.fr33domlover.site/repos/9nOkn",
        "name": "main",
        "ref": "/refs/heads/main"
    },
    "capability": "https://fig.fr33domlover.site/looms/9nOkn/outbox/kDJx0",
    "to": [
        "https://fig.fr33domlover.site/looms/9nOkn",
        "https://fig.fr33domlover.site/people/qn870/followers",
        "https://fig.fr33domlover.site/looms/9nOkn/followers",
        "https://fig.fr33domlover.site/looms/9nOkn/cloths/mbWob/followers"
    ]
}
</xmp>
</div>

### Actors

#### Software development components

<pre class=simpledef>
Name: <dfn dfn export>Repository</dfn>
URI: https://forgefed.org/ns#Repository
Extends: [=Object=]
Description:
    Represents a version control system repository.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=8>
{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://w3id.org/security/v1",
        "https://forgefed.org/ns"
    ],
    "id": "https://dev.example/aviva/treesim",
    "type": "Repository",
    "publicKey": {
        "id": "https://dev.example/aviva/treesim#main-key",
        "owner": "https://dev.example/aviva/treesim",
        "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhki....."
    },
    "inbox": "https://dev.example/aviva/treesim/inbox",
    "outbox": "https://dev.example/aviva/treesim/outbox",
    "followers": "https://dev.example/aviva/treesim/followers",
    "team": "https://dev.example/aviva/treesim/team",
    "name": "Tree Growth 3D Simulation",
    "summary": "<p>Tree growth 3D simulator for my nature exploration game</p>"
}
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>TicketTracker</dfn>
URI: https://forgefed.org/ns#TicketTracker
Extends: [=Object=]
Description:
    Represents a [[wikipedia-ticket-tracker|ticket tracker]], i.e. a project
    managing a list of work items.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=8>
{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://w3id.org/security/v2",
        "https://forgefed.org/ns"
    ],
    "id": "https://dev.example/aviva/treesim",
    "type": ["Repository", "TicketTracker"],
    "publicKey": {
        "id": "https://dev.example/aviva/treesim#main-key",
        "owner": "https://dev.example/aviva/treesim",
        "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhki....."
    },
    "inbox": "https://dev.example/aviva/treesim/inbox",
    "outbox": "https://dev.example/aviva/treesim/outbox",
    "followers": "https://dev.example/aviva/treesim/followers",
    "name": "Tree Growth 3D Simulation",
    "summary": "<p>Tree growth 3D simulator for my nature exploration game</p>"
}
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>PatchTracker</dfn>
URI: https://forgefed.org/ns#PatchTracker
Extends: [=Object=]
Description:
    Represents a tracker of [[wikipedia-pr|merge requests]], i.e. a project
    managing a list of patches or branches submitted as proposed changes to a
    given [=Repository=].
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=8>
{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://w3id.org/security/v2",
        "https://forgefed.org/ns"
    ],
    "id": "https://dev.example/aviva/treesim",
    "type": ["Repository", "TicketTracker", "PatchTracker"],
    "publicKey": {
        "id": "https://dev.example/aviva/treesim#main-key",
        "owner": "https://dev.example/aviva/treesim",
        "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhki....."
    },
    "inbox": "https://dev.example/aviva/treesim/inbox",
    "outbox": "https://dev.example/aviva/treesim/outbox",
    "followers": "https://dev.example/aviva/treesim/followers",
    "name": "Tree Growth 3D Simulation",
    "summary": "<p>Tree growth 3D simulator for my nature exploration game</p>"
}
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>Workflow</dfn>
URI: https://forgefed.org/ns#Workflow
Extends: [=Object=]
Description:
    Manages a set of custom labels and properties of tickets and MRs, as well
    as the possible transitions between property values.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=8>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>Roadmap</dfn>
URI: https://forgefed.org/ns#Roadmap
Extends: [=Object=]
Description:
    Manages a set of project milestones under which issues and MRs can be
    grouped.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=8>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>ReleaseTracker</dfn>
URI: https://forgefed.org/ns#ReleaseTracker
Extends: [=Object=]
Description:
    Manages a set of software releases for a given [=Repository=]
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=8>
TODO
</xmp>
</div>

#### Organizational structure tools

<pre class=simpledef>
Name: <dfn dfn export>Project</dfn>
URI: https://forgefed.org/ns#Project
Extends: [=Object=]
Description:
    Represents a project, a planned endeavor that involves usage of tools
    related to the software development lifecycle. It may be a software
    project, but may also be totally unrelated to software development. For
    example, it may be a book that is being written using Markdown files kept
    in a Git repository. A [=Project=] object is a way to collect forge related
    components together under one title.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=8>
{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://w3id.org/security/v2",
        "https://forgefed.org/ns"
    ],
    "id": "https://dev.example/projects/wanderer",
    "type": "Project",

    "name": "Wanderer",
    "summary": "3D nature exploration game",
    "components": {
        "type": "Collection",
        "totalItems": 7,
        "items": [
            "https://dev.example/repos/opengl-vegetation",
            "https://dev.example/repos/opengl-vegetation/patch-tracker",
            "https://dev.example/repos/treesim",
            "https://dev.example/repos/treesim/patch-tracker",
            "https://dev.example/repos/wanderer",
            "https://dev.example/repos/wanderer/patch-tracker",
            "https://dev.example/issue-trackers/wanderer"
        ]
    },

    "publicKey": {
        "id": "https://dev.example/projects/wanderer#main-key",
        "owner": "https://dev.example/projects/wanderer",
        "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhki....."
    },
    "inbox": "https://dev.example/projects/wanderer/inbox",
    "outbox": "https://dev.example/projects/wanderer/outbox",
    "followers": "https://dev.example/projects/wanderer/followers"
}
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>Team</dfn>
URI: https://forgefed.org/ns#Team
Extends: [=Object=]
Description:
    Represents a group of people working together, collaborating on shared
    resources. Each member [=Person=] in the team has a defined role, affecting
    the level of access they have to the team's shared resources and to
    managing the team itself.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=8>
{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://w3id.org/security/v2",
        "https://forgefed.org/ns"
    ],
    "id": "https://dev.example/teams/mobilizon-dev-team",
    "type": "Team",
    "name": "Mobilizon Development Team",
    "summary": "We're creating a federated tool for organizing events!",
    "members": {
        "type": "Collection",
        "totalItems": 3,
        "items": [
            { "type": "Relationship",
              "subject": "https://dev.example/teams/mobilizon-dev-team",
              "relationship": "hasMember",
              "object": "https://dev.example/people/alice",
              "tag": "https://roles.example/admin"
            },
            { "type": "Relationship",
              "subject": "https://dev.example/teams/mobilizon-dev-team",
              "relationship": "hasMember",
              "object": "https://dev.example/people/bob",
              "tag": "maintain"
            },
            { "type": "Relationship",
              "subject": "https://dev.example/teams/mobilizon-dev-team",
              "relationship": "hasMember",
              "object": "https://dev.example/people/celine",
              "tag": "develop"
            }
        ]
    },
    "subteams": {
        "type": "Collection",
        "totalItems": 2,
        "items": [
            "https://dev.example/teams/mobilizon-backend-team",
            "https://dev.example/teams/mobilizon-frontend-team"
        ]
    },
    "context": "https://dev.example/teams/framasoft-developers",

    "publicKey": {
        "id": "https://dev.example/teams/mobilizon-dev-team#main-key",
        "owner": "https://dev.example/teams/mobilizon-dev-team",
        "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhki....."
    },
    "inbox": "https://dev.example/teams/mobilizon-dev-team/inbox",
    "outbox": "https://dev.example/teams/mobilizon-dev-team/outbox",
    "followers": "https://dev.example/teams/mobilizon-dev-team/followers"
}
</xmp>
</div>

#### Utility

<pre class=simpledef>
Name: <dfn dfn export>Factory</dfn>
URI: https://forgefed.org/ns#Factory
Extends: [=Object=]
Description:
  A service actor that creates resource actors (such as
  [=Repository=]), exists to allow access control on creation of remote
  resources.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=11>
{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://w3id.org/security/v2",
        "https://forgefed.org/ns"
    ],
    "id": "https://grape.fr33domlover.site/factories/YGQoZ",
    "type": "Factory",
    "name": "final-factory",
    "summary": ".",
    "availableActorTypes": [
        "TicketTracker",
        "Project",
        "Team",
        "Repository"
    ],

    "outbox": "https://grape.fr33domlover.site/factories/YGQoZ/outbox",
    "publicKey": [
        "https://grape.fr33domlover.site/akey1",
        "https://grape.fr33domlover.site/akey2"
    ],
    "collaborators": "https://grape.fr33domlover.site/factories/YGQoZ/collabs",
    "followers": "https://grape.fr33domlover.site/factories/YGQoZ/followers",
    "inbox": "https://grape.fr33domlover.site/factories/YGQoZ/inbox",
    "teams": "https://grape.fr33domlover.site/factories/YGQoZ/teams"
}
</xmp>
</div>

### Objects

<pre class=simpledef>
Name: <dfn dfn export>CapabilityUsage</dfn>
URI: https://forgefed.org/ns#CapabilityUsage
Extends: [=Object=]
Values:
    This specification defines 3&#x3a; [=gatherAndConvey=], [=distribute=] and
    [=invoke=]
Description:
    Represents a mode of using a [=Grant=] as an Object Capability (OCAP).
    There are two conceptual operations for `Grant`s&#x3a; Invocation (acting
    on the resource under the specified role) and Delegation (passing on the
    access to more actors, possibly with reduced privileges). A value of this
    type refers to one or both of these operations, and possibly to more
    specific conditions and restrictions on applying them.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=7>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>Role</dfn>
URI: https://forgefed.org/ns#Role
Extends: [=Object=]
Values:
    [=visit=], [=report=], [=triage=], [=write=], [=maintain=], [=admin=],
    [=delegate=]
Description:
    Represents a role that an actor has within a [=Team=], or a role defining
    the level of access an actor has to a resource.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=7>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>Branch</dfn>
URI: https://forgefed.org/ns#Branch
Extends: [=Object=]
Description:
    Represents a named variable reference to a version of the [=Repository=],
    typically used for committing changes in parallel to other development, and
    usually eventually merging the changes into the main history line.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=7>
{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://forgefed.org/ns"
    ],
    "id": "https://example.dev/luke/myrepo/branches/master",
    "type": "Branch",
    "name": "master",
    "context": "https://example.dev/luke/myrepo",
    "ref": "refs/heads/master"
}
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>Commit</dfn>
URI: https://forgefed.org/ns#Commit
Extends: [=Object=]
Description:
    Represents a named set of changes in the history of a [=Repository=].  This
    is called "commit" in Git, Mercurial and Monotone; "patch" in Darcs;
    sometimes called "change set". Note that `Commit` is a set of changes that
    already exists in a repo's history, while a [=Patch=] is a separate
    proposed change set, that *could* be applied and pushed to a repo,
    resulting with a `Commit`.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=7>
{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://forgefed.org/ns"
    ],
    "id": "https://example.dev/alice/myrepo/commits/109ec9a09c7df7fec775d2ba0b9d466e5643ec8c",
    "type": "Commit",
    "context": "https://example.dev/alice/myrepo",
    "attributedTo": "https://example.dev/bob",
    "committedBy": "https://example.dev/alice",
    "hash": "109ec9a09c7df7fec775d2ba0b9d466e5643ec8c",
    "summary": "Add an installation script, fixes issue #89",
    "description": {
        "mediaType": "text/plain",
        "content": "It's about time people can install on their computers!"
    },
    "created": "2019-07-11T12:34:56Z",
    "committed": "2019-07-26T23:45:01Z"
}
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>Patch</dfn>
URI: https://forgefed.org/ns#Patch
Extends: [=Object=]
Description:
    Represents a named set of changes that are being proposed for applying to a
    specific [=Repository=].
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=7>
{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://forgefed.org/ns"
    ],
    "id": "https://dev.example/aviva/game-of-life/pulls/825/versions/1/patches/1",
    "type": "Patch",
    "attributedTo": "https://forge.example/luke",
    "context": "https://dev.example/aviva/game-of-life/pulls/825/versions/1",
    "mediaType": "application/x-git-patch",
    "content": "From c9ae5f4ff4a330b6e1196ceb7db1665bd4c1..."
}
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>TicketDependency</dfn>
URI: https://forgefed.org/ns#TicketDependency
Extends: [=Relationship=]
Description:
    Represents a relationship between 2 [=Ticket=]s, in which the resolution of
    one ticket requires the other ticket to be resolved too. It MUST specify
    the [=subject=], [=object=] and [=relationship=] properties, and the
    `relationship` property MUST be [=dependsOn=].
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=6>
{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://forgefed.org/ns"
    ],
    "type": ["Relationship", "TicketDependency"],
    "id": "https://example.dev/ticket-deps/2342593",
    "attributedTo": "https://example.dev/alice",
    "summary": "Alice's ticket depends on Bob's ticket",
    "published": "2019-07-11T12:34:56Z",
    "subject": "https://example.dev/alice/myproj/issues/42",
    "relationship": "dependsOn",
    "object": "https://dev.community/bob/coolproj/issues/85"
}
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>Ticket</dfn>
URI: https://forgefed.org/ns#Ticket
Extends: [=Object=]
Description:
    Represents an item that requires work or attention. Tickets exist in the
    context of a project (which may or may not be a version-control
    repository), and are used to track ideas, proposals, tasks, bugs and more.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=6>
{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://forgefed.org/ns"
    ],
    "type": "Ticket",
    "id": "https://example.dev/alice/myrepo/issues/42",
    "context": "https://example.dev/alice/myrepo",
    "attributedTo": "https://dev.community/bob",
    "summary": "Nothing works!",
    "content": "<p>Please fix. <i>Everything</i> is broken!</p>",
    "mediaType": "text/html",
    "source": {
        "content": "Please fix. *Everything* is broken!",
        "mediaType": "text/markdown; variant=CommonMark"
    },
    "assignments": "https://example.dev/alice/myrepo/issues/42/assignments",
    "isResolved": false
}
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>Enum</dfn>
URI: https://forgefed.org/ns#Enum
Extends: [=Object=]
Description:
    Represents a set of named values.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=6>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>EnumValue</dfn>
URI: https://forgefed.org/ns#EnumValue
Extends: [=Object=]
Description:
    Represents a single named value within an [=Enum=].
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=6>
TODO
</xmp>
</div>

<!--

<pre class=simpledef>
Name: <dfn dfn export>EnumTransition</dfn>
URI: https://forgefed.org/ns#EnumTransition
Extends: [=Object=]
Description:
    Represents an allowed transition between values of a specific [=Enum=].
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=6>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>EnumTransitionSet</dfn>
URI: https://forgefed.org/ns#EnumTransitionSet
Extends: [=Object=]
Description:
    Represents a set of allowed transitions between values of a specific
    [=Enum=].
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=6>
TODO
</xmp>
</div>

-->

<pre class=simpledef>
Name: <dfn dfn export>Field</dfn>
URI: https://forgefed.org/ns#Field
Extends: [=Object=]
Description:
    Represents a custom ticket field within a [=Workflow=].
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=6>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>FieldType</dfn>
URI: https://forgefed.org/ns#FieldType
Extends: [=Object=]
Description:
    Represents the type of a [=Field=].
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=6>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>FieldValue</dfn>
URI: https://forgefed.org/ns#FieldValue
Extends: [=Object=]
Description:
    Specifies a value of a [=Field=].
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=6>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>Milestone</dfn>
URI: https://forgefed.org/ns#Milestone
Extends: [=Object=]
Description:
    Describes a project milestone, under which issues and MRs can be grouped.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=6>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>Release</dfn>
URI: https://forgefed.org/ns#Release
Extends: [=Object=]
Description:
    Describes a software release.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=6>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>ReviewVerdict</dfn>
URI: https://forgefed.org/ns#ReviewVerdict
Extends: [=Object=]
Values: [=approve=], [=remark=], [=revise=]
Description:
    Describes the level of approval given by a review.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=6>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>ReviewStatus</dfn>
URI: https://forgefed.org/ns#ReviewStatus
Extends: [=ReviewVerdict=]
Values:
    [=approve=], [=remark=], [=revise=],
    [=awaiting=], [=dismissed=], [=dismissedAwaiting=]
Description:
    Describes the status of the review.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=6>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>ReviewThread</dfn>
URI: https://forgefed.org/ns#ReviewThread
Extends: [=Object=]
Description:
    A comment on a code change and the discussion on it.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=6>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>Suggestion</dfn>
URI: https://forgefed.org/ns#Suggestion
Extends: [=Object=]
Description:
    A suggested edit within a review on a merge request
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=6>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>CodeQuote</dfn>
URI: https://forgefed.org/ns#CodeQuote
Extends: [=Object=]
Description:
    A reference to a part of a merge request diff being reviewed
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=6>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>Approval</dfn>
URI: https://forgefed.org/ns#Approval
Extends: [=Object=]
Description:
    The status of a review or review request on a merge request
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=6>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>DiffSide</dfn>
URI: https://forgefed.org/ns#DiffSide
Extends: [=Object=]
Values: [=diffSideOld=], [=diffSideNew=]
Description:
    Refers to either the old or new version of a file being changed.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=6>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>Review</dfn>
URI: https://forgefed.org/ns#Review
Extends: [=Object=]
Description:
    Describes a review on a Merge Request.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=6>
TODO
</xmp>
</div>

## Properties

### General-purpose

<pre class=simpledef>
Name: <dfn dfn export>earlyItems</dfn>
URI: https://forgefed.org/ns#earlyItems
Domain: [=OrderedCollection=]
Range: Ordered list of [ [=Object=] | [=Link=] ]
Functional: No
Inverse of: None
Description:
    In an ordered collection (or an ordered collection page) in which [=items=]
    (or [=orderedItems=]) contains a continuous subset of the collection's
    items from one end, `earlyItems` identifiers a continuous subset from the
    other end. For example, if `items` lists the chronologically latest items,
    `earlyItems` would list the chrologically earliest items. The ordering rule
    for items in `earlyItems` MUST be the same as in `items`. For examle, if
    `items` lists items in reverse chronogical order, then so does
    `earlyItems`.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=14>
{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://forgefed.org/ns"
    ],
    "id": "https://dev.example/aviva/outbox",
    "type": "OrderedCollection",
    "totalItems": 712,
    "orderedItems": [
       "https://dev.example/aviva/outbox/712",
       "https://dev.example/aviva/outbox/711",
       "https://dev.example/aviva/outbox/710"
    ],
    "earlyItems": [
       "https://dev.example/aviva/outbox/3",
       "https://dev.example/aviva/outbox/2",
       "https://dev.example/aviva/outbox/1"
    ]
}
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>previousVersions</dfn>
URI: https://forgefed.org/ns#previousVersions
Domain: [=Object=]
Range: `rdf:List` of objects of the same `@type` as the subject
Functional: Yes
Inverse of: None, but see [=currentVersion=]
Description:
    Specifies the previous versions of the subject, as an ordered list in
    reverse chronological order.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=10>
{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://forgefed.org/ns"
    ],
    "id": "https://dev.example/aviva/notes/107",
    "type": "Note",
    "attributedTo": "https://dev.example/aviva",
    "content": "I agree!!!!! (edit: fixed a typo)",
    "previousVersions": [
       "https://dev.example/aviva/notes/107_old_version",
       "https://dev.example/aviva/notes/107_very_old_version",
       "https://dev.example/aviva/notes/107_ancient_version"
    ]
}
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>currentVersion</dfn>
URI: https://forgefed.org/ns#currentVersion
Domain: [=Object=]
Range: [=Object=], of the same `@type` as the subject
Functional: Yes
Inverse of: None, but see [=previousVersions=]
Description:
    Specifies the latest. current, up-to-date version of the subject.  Once the
    subject specifies the `currentVersion` property, it SHOULD NOT get any
    changes to any other properties. The exception is `currentVersion` itself,
    which MUST be updated whenever needed, to always point to the latest
    version.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=10>
{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://forgefed.org/ns"
    ],
    "id": "https://dev.example/aviva/notes/107_old_version",
    "type": "Note",
    "attributedTo": "https://dev.example/aviva",
    "content": "I agree!!111",
    "currentVersion": "https://dev.example/aviva/notes/107"
}
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>originPosition</dfn>
URI: https://forgefed.org/ns#originPosition
Domain: [=Move=]
Range: `xsd:nonNegativeInteger`
Functional: Yes
Inverse of: None
Description:
    When moving an element in an ordered list, indicates the origin position
    from which the element is moved.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=10>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>targetPositionAfter</dfn>
URI: https://forgefed.org/ns#targetPositionAfter
Domain: [=Move=]
Range: `xsd:nonNegativeInteger`
Functional: Yes
Inverse of: None
Description:
    Indicates in which position in a list to place a certain element. 1 means
    "place after the element that is currently the first", 2 means "place after
    the element that is currently the 2nd", and so on. 0 means "place before
    the first element".
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=10>
TODO
</xmp>
</div>

### Factories

<pre class=simpledef>
Name: <dfn dfn export>availableActorTypes</dfn>
URI: https://forgefed.org/ns#availableActorTypes
Domain: [=Factory=]
Range: URI
Functional: No
Inverse of: None
Description: Specifies the actor types that can be created via this Factory.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=11>
{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://w3id.org/security/v2",
        "https://forgefed.org/ns"
    ],
    "id": "https://grape.fr33domlover.site/factories/YGQoZ",
    "type": "Factory",
    "name": "final-factory",
    "summary": ".",
    "availableActorTypes": [
        "TicketTracker",
        "Project",
        "Team",
        "Repository"
    ],

    "outbox": "https://grape.fr33domlover.site/factories/YGQoZ/outbox",
    "publicKey": [
        "https://grape.fr33domlover.site/akey1",
        "https://grape.fr33domlover.site/akey2"
    ],
    "collaborators": "https://grape.fr33domlover.site/factories/YGQoZ/collabs",
    "followers": "https://grape.fr33domlover.site/factories/YGQoZ/followers",
    "inbox": "https://grape.fr33domlover.site/factories/YGQoZ/inbox",
    "teams": "https://grape.fr33domlover.site/factories/YGQoZ/teams"
}
</xmp>
</div>

### Nesting

<pre class=simpledef>
Name: <dfn dfn export>hasChild</dfn>
URI: https://forgefed.org/ns#hasChild
Domain: [=Project=] | [=Team=]
Range: [=Project=] | [=Team=]
Functional: No
Inverse of: [=hasParent=]
Description:
    For a given project or team *A*, specifies an actor of the same type *B*
    who is a child of *A* (a project from whom *A* receives delegations, or a
    team to whom *B* extends delegations).
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=11>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>hasParent</dfn>
URI: https://forgefed.org/ns#hasParent
Domain: [=Project=] | [=Team=]
Range: [=Project=] | [=Team=]
Functional: No
Inverse of: [=hasChild=]
Description:
    For a given project or team *A*, specifies an actor of the same type *B*
    who is a parent of *A* (a team from whom *A* receives delegations, or a
    project to whom *B* extends delegations).
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=11>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>hasComponent</dfn>
URI: https://forgefed.org/ns#hasComponent
Domain: [=Project=]
Range: An actor of a component type
Functional: No
Inverse of: [=componentOf=]
Description:
    For a given project *A*, specifies one of its components
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=11>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>componentOf</dfn>
URI: https://forgefed.org/ns#componentOf
Domain: An actor of a component type
Range: [=Project=]
Functional: No
Inverse of: [=hasComponent=]
Description:
    For a given component *A*, specifies one of the projects it belongs to
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=11>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>hasOverseen</dfn>
URI: https://forgefed.org/ns#hasOverseen
Domain: [=Team=]
Range: [=Team=]
Functional: No
Inverse of: [=hasOversight=]
Description:
    For a given team *A*, specifies a team *B* who has delegated access-to-*B*
    to *A*.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=11>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>hasOversight</dfn>
URI: https://forgefed.org/ns#hasOversight
Domain: [=Team=]
Range: [=Team=]
Functional: No
Inverse of: [=hasOverseen=]
Description:
    For a given team *A*, specifies a team *B* to whom who *A* has delegated
    access-to-*A*
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=11>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>oversees</dfn>
URI: https://forgefed.org/ns#oversees
Domain: [=Team=]
Range: [=Collection=] of [=Relationship=]s with other [=Team=]s.
Functional: Yes
Inverse of: None, but see [=overseenBy=]
Description:
    Teams overseen by this team. The collection items are [=Relationship=]
    objects whose [=relationship=] is [=hasOverseen=] and whose [=instrument=]
    is the maximal role allowed for delegation, specified when the overseen
    team was added.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=9>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>overseenBy</dfn>
URI: https://forgefed.org/ns#overseenBy
Domain: [=Team=]
Range: [=Collection=] of [=Relationship=]s with other [=Team=]s.
Functional: Yes
Inverse of: None, but see [=oversees=]
Description:
    Teams by whom this team is being overseen. The collection items are
    [=Relationship=] objects whose [=relationship=] is [=hasOversight=] and
    whose [=instrument=] is the maximal role allowed for delegation, specified
    when the overseen team was added.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=9>
TODO
</xmp>
</div>

### Projects

<pre class=simpledef>
Name: <dfn dfn export>components</dfn>
URI: https://forgefed.org/ns#components
Domain: [=Project=]
Range: [=Collection=]
Functional: Yes
Inverse of:
    None, but see the usage of [=context=] in e.g. [[#model-repo]]
Description:
    Identifies a [=Collection=] listing actors whose services and resources are
    considered to be components of this project. The collection items are
    [=Relationship=] objects whose [=relationship=] is [=hasComponent=] and
    whose [=instrument=] is the maximal role allowed for delegation, specified
    when the component was added.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=11>
{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://forgefed.org/ns"
    ],
    "id": "https://dev.example/projects/wanderer",
    "type": "Project",

    "name": "Wanderer",
    "summary": "3D nature exploration game",
    "components": {
        "type": "Collection",
        "totalItems": 3,
        "items": [
            { "type": "Relationship",
              "subject": "https://dev.example/projects/wanderer",
              "relationship": "hasComponent",
              "object": "https://dev.example/repos/opengl-vegetation",
              "instrument": "admin"
            },
            { "type": "Relationship",
              "subject": "https://dev.example/projects/wanderer",
              "relationship": "hasComponent",
              "object": "https://dev.example/repos/opengl-vegetation/patch-tracker",
              "instrument": "maintain"
            },
            { "type": "Relationship",
              "subject": "https://dev.example/projects/wanderer",
              "relationship": "hasComponent",
              "object": "https://dev.example/repos/treesim",
              "instrument": "write"
            }
        ]
    },

    "inbox": "https://dev.example/projects/wanderer/inbox",
    "outbox": "https://dev.example/projects/wanderer/outbox",
    "followers": "https://dev.example/projects/wanderer/followers"
}
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>subprojects</dfn>
URI: https://forgefed.org/ns#subprojects
Domain: [=Project=]
Range: [=Collection=]
Functional: Yes
Inverse of:
    None, but see the usage of [=context=] in [[#model-project]]
Description:
    Identifies a [=Collection=] listing the subprojects of this [=Project=].
    The collection items are [=Relationship=] objects whose [=relationship=] is
    [=hasChild=] and whose [=instrument=] is the maximal role allowed for
    delegation, specified when the child was added.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=11>
{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://forgefed.org/ns"
    ],
    "id": "https://dev.example/projects/wanderer",
    "type": "Project",

    "name": "Wanderer",
    "summary": "3D nature exploration game",
    "subprojects": {
        "type": "Collection",
        "totalItems": 2,
        "items": [
            { "type": "Relationship",
              "subject": "https://dev.example/projects/wanderer",
              "relationship": "hasChild",
              "object": "https://dev.example/projects/nature-3d-models",
              "instrument": "admin"
            },
            { "type": "Relationship",
              "subject": "https://dev.example/projects/wanderer",
              "relationship": "hasChild",
              "object": "https://dev.example/projects/wanderer-fundraising"
              "instrument": "write"
            }
        ]
    },

    "inbox": "https://dev.example/projects/wanderer/inbox",
    "outbox": "https://dev.example/projects/wanderer/outbox",
    "followers": "https://dev.example/projects/wanderer/followers"
}
</xmp>
</div>

### Team membership and nesting

<div class=example>
<xmp highlight=json-ld line-highlight=9>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>subteams</dfn>
URI: https://forgefed.org/ns#subteams
Domain: [=Team=]
Range: [=Collection=] of [=Team=]s.
Functional: Yes
Inverse of: None
Description:
    Identifies a collection of the subteams of this [=Team=], i.e. teams whose
    members inherit the access that this team's members have to projects and to
    project components (such as [=Repository=]s).
    The collection items are [=Relationship=] objects whose [=relationship=] is
    [=hasChild=] and whose [=instrument=] is the maximal role allowed for
    delegation, specified when the child was added.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=9>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>hasRecursiveCollaborator</dfn>
URI: https://forgefed.org/ns#hasRecursiveCollaborator
Domain: Resource actor that isn't [=Team=]
Range: [=Team=]
Functional: No
Inverse of: None
Description:
    Identifier a [=Team=] that's been granted access to this resource.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=9>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>teams</dfn>
URI: https://forgefed.org/ns#teams
Domain: An actor of type that isn't [=Team=]
Range:
    [=Collection=] of [=Relationship=] objects, in each of which [=subject=] is
    a resource actor's URI, [=relationship=] is [=hasRecursiveCollaborator=],
    [=object=] is a Team's URI and [=instrument=] is the role given to that
    team in that resource.
Functional: Yes
Inverse of: None (but see [=teamResources=])
Description:
    Identifies a collection of the resources this [=Team=] has access to, and
    that it is delegating to its members and subteams.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=9>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>teamResources</dfn>
URI: https://forgefed.org/ns#teamResources
Domain: [=Team=]
Range:
    [=Collection=] of [=Relationship=] objects, in each of which [=subject=] is
    a resource actor's URI, [=relationship=] is [=hasRecursiveCollaborator=],
    [=object=] is a Team's URI and [=instrument=] is the role given to that
    team in that resource.
Functional: Yes
Inverse of: None (but see [=teams=])
Description:
    Identifies a collection of the [=Team=]s that have access to this resource,
    and are delegating it to their members and subteams.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=9>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>roleFilter</dfn>
URI: https://forgefed.org/ns#roleFilter
Domain: [=Team=]
Range:
    JSON object in which each key is either a component type or [=Project=],
    and each key maps to exactly one value, that is either `null` or a [=Role=]
    that isn't [=delegate=] and isn't [=admin=]
Functional: Yes
Inverse of: None
Description:
    If a [=Team=] specifies a role filter, this filter is now used for
    attenuating the [=Grant=]s being extended to the team's members
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=9>
TODO
</xmp>
</div>

### Ticket assignment and resolution

<pre class=simpledef>
Name: <dfn dfn export>assignedTo</dfn>
URI: https://forgefed.org/ns#assignedTo
Domain: [=Ticket=]
Range: [=Person=] or [=Team=]
Functional: No
Inverse of: None
Description:
    Identifies a [=Person=] or [=Team=] assigned to work on this [=Ticket=].
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=9>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>assignments</dfn>
URI: https://forgefed.org/ns#assignments
Domain: [=Ticket=]
Range:
    [=OrderedCollection=] of [=Relationship=] objects, in each of which
    [=subject=] is the Ticket, [=relationship=] is [=assignedTo=] and
    [=object=] is a [=Person=] or [=Team=]
Functional: Yes
Inverse of: None
Description:
    Identifies the [=Person=]s and [=Team=]s assigned to work on this
    [=Ticket=].
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=9>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>isResolved</dfn>
URI: https://forgefed.org/ns#isResolved
Domain: [=Ticket=] | [=Milestone=] | [=ReviewThread=]
Range: `xsd:boolean`
Functional: Yes
Inverse of: None
Description:
    Specifies whether the [=Ticket=] is closed, i.e. the work on it is done and
    it doesn't need to attract attention anymore.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=9>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>resolvedBy</dfn>
URI: https://forgefed.org/ns#resolvedBy
Domain: [=Ticket=] | [=Milestone=] | [=ReviewThread=]
Range: [=Object=] than is an actor, or [=Activity=]
Functional: Yes
Inverse of: None
Description:
    Identifies the Actor who has resolved the [=Ticket=], or the activity that
    has resolved the Ticket.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=9>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>resolved</dfn>
URI: https://forgefed.org/ns#resolved
Domain: [=Ticket=] | [=Milestone=] | [=ReviewThread=]
Range: `xsd:dateTime`
Functional: Yes
Inverse of: None
Description:
    For a resolved [=Ticket=], specifies the time the Ticket has been resolved.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=9>
TODO
</xmp>
</div>

### Ticket dependencies

<pre class=simpledef>
Name: <dfn dfn export>dependsOn</dfn>
URI: https://forgefed.org/ns#dependsOn
Domain: [=Ticket=]
Range: [=Ticket=]
Functional: No
Inverse of: [=dependedBy=]
Description:
    Identifies one or more tickets on which this [=Ticket=] depends, i.e. it
    can't be resolved without those tickets being resolved too.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=9>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>dependedBy</dfn>
URI: https://forgefed.org/ns#dependedBy
Domain: [=Ticket=]
Range: [=Ticket=]
Functional: No
Inverse of: [=dependsOn=]
Description:
    Identifies one or more tickets which depend on this [=Ticket=], i.e. they
    can't be resolved without this tickets being resolved too.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=9>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>dependencies</dfn>
URI: https://forgefed.org/ns#dependencies
Domain: [=Ticket=]
Range: [=Collection=] of items of type [=TicketDependency=]
Functional: Yes
Inverse of: None
Description:
    Identifies a [=Collection=] of [=TicketDependency=] which specify tickets
    that this [=Ticket=] depends on, i.e. this ticket is the [=subject=] of the
    [=dependsOn=] relationship.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=9>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>dependants</dfn>
URI: https://forgefed.org/ns#dependants
Domain: [=Ticket=]
Range: [=Collection=] of items of type [=TicketDependency=]
Functional: Yes
Inverse of: None
Description:
    Identifies a [=Collection=] of [=TicketDependency=] which specify tickets
    that depends on this [=Ticket=], i.e. this ticket is the [=object=] of the
    [=dependsOn=] relationship. Often called "reverse dependencies".
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=9>
TODO
</xmp>
</div>

### Ticket fields

<pre class=simpledef>
Name: <dfn dfn export>customFields</dfn>
URI: https://forgefed.org/ns#customFields
Domain: [=Object=]
Range: [=FieldValue=]
Functional: No
Inverse of: None
Description:
    Specifies a custom field on a ticket.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=9>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export lt="prop:fieldValue">fieldValue</dfn>
URI: https://forgefed.org/ns#fieldValue
Domain: [=FieldValue=]
Range: Any
Functional: Yes
Inverse of: None
Description:
    Specifies the value ofa custom field.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=9>
TODO
</xmp>
</div>

### Repository cloning

<pre class=simpledef>
Name: <dfn dfn export>cloneUri</dfn>
URI: https://forgefed.org/ns#cloneUri
Domain: [=Repository=]
Range: [=Object=]
Functional: No
Inverse of: None
Description:
    An endpoint that can be used with a VCS protocol such as Git or Mercurial
    to access a given repository.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=9>
TODO
</xmp>
</div>

### Repository pushing

<pre class=simpledef>
Name: <dfn dfn export>hashBefore</dfn>
URI: https://forgefed.org/ns#hashBefore
Domain: [=Push=]
Range: `xsd:string` of hexadecimal digit ASCII characters
Functional: Yes
Inverse of: None
Description:
    Specifies the hash of the commit that the pushed branch/repo was pointing
    to *right before* the push happpened.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=9>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>hashAfter</dfn>
URI: https://forgefed.org/ns#hashAfter
Domain: [=Push=]
Range: `xsd:string` of hexadecimal digit ASCII characters
Functional: Yes
Inverse of: None
Description:
    Specifies the hash of the commit that the pushed branch/repo was pointing
    to *right after* the push happpened.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=9>
TODO
</xmp>
</div>

### Commits and branches

<pre class=simpledef>
Name: <dfn dfn export lt="prop-repository">repository</dfn> (DEPRECATED)
URI: https://forgefed.org/ns#repository
Domain: [=Commit=]
Range: [=Repository=]
Functional: Yes
Inverse of: None
Description:
    Identifies the repository to which a commit belongs. DEPRECATED: Use the
    standard ActivityPub `context` property instead.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=9>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>description</dfn>
URI: https://forgefed.org/ns#description
Domain: [=Commit=]
Range:
    [=Object=], specifying [=content=] and [=mediaType=]. The `mediaType`
    SHOULD be `"text/plain"`.
Functional: Yes
Inverse of: None
Description:
    Specifies the description text of a [=Commit=], which is an optional
    possibly multi-line text provided in addition to the one-line commit title.
    The range of the `description` property works the same way the range of the
    ActivityPub [=source=] property works.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=14>
{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://forgefed.org/ns"
    ],
    "id": "https://example.dev/alice/myrepo/commits/109ec9a09c7df7fec775d2ba0b9d466e5643ec8c",
    "type": "Commit",
    "context": "https://example.dev/alice/myrepo",
    "attributedTo": "https://example.dev/bob",
    "hash": "109ec9a09c7df7fec775d2ba0b9d466e5643ec8c",
    "created": "2019-07-11T12:34:56Z",
    "summary": "Add an installation script, fixes issue #89",

    "description": {
        "mediaType": "text/plain",
        "content": "It's about time people can install on their computers!"
    },
}
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>committedBy</dfn>
URI: https://forgefed.org/ns#committedBy
Domain: [=Commit=]
Range: [=Object=]
Functional: Yes
Inverse of: None
Description:
    Identifies the actor (usually a person, but could be something else, e.g. a
    bot) that added a set of changes to the version-control [=Repository=].
    Sometimes the author of the changes and the committer of those changes
    aren't the same actor, in which case the `committedBy` property can be used
    to specify who added the changes to the repository. For example, when
    applying a patch to a repository, e.g. a Git repository, the author would
    be the person who made the patch, and the committer would be the person who
    applied the patch to their copy of the repository.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=9>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>hash</dfn>
URI: https://forgefed.org/ns#hash
Domain: [=Commit=]
Range: `xsd:string` of hexadecimal digit ASCII characters
Functional: Yes
Inverse of: None
Description:
    Specifies the hash associated with a [=Commit=], which is a unique
    identifier of the commit within the [=Repository=], usually generated as a
    cryptographic hash function of some (or all) of the commit's data or
    metadata.  For example, in Git it would be the SHA1 hash of the commit; in
    Darcs it would be the SHA1 hash of the patch info.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=9>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>committed</dfn>
URI: https://forgefed.org/ns#committed
Domain: [=Commit=]
Range: `xsd:dateTime`
Functional: Yes
Inverse of: None
Description:
    Specifies the time that a set of changes was committed into the
    [=Repository=] and became a [=Commit=] in it. This can be different from
    the time the set of changes was produced, e.g. if one person creates a
    patch and sends to another, and the other person then applies the patch to
    their copy of the repository. We call the former event "created" and the
    latter event "committed", and this latter event is specified by the
    `committed` property.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=9>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>filesAdded</dfn>
URI: https://forgefed.org/ns#filesAdded
Domain: [=Commit=]
Range: `xsd:string`
Functional: No
Inverse of: None
Description:
    Specifies a filename, as a relative path, relative to the top of the tree
    of files in the [=Repository=], of a file that got added in this
    [=Commit=], and didn't exist in the previous version of the tree.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=9>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>filesModified</dfn>
URI: https://forgefed.org/ns#filesModified
Domain: [=Commit=]
Range: `xsd:string`
Functional: No
Inverse of: None
Description:
    Specifies a filename, as a relative path, relative to the top of the tree
    of files in the [=Repository=], of a file that existed in the previous
    version of the tree, and its contents got modified in this [=Commit=].
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=9>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>filesRemoved</dfn>
URI: https://forgefed.org/ns#filesRemoved
Domain: [=Commit=]
Range: `xsd:string`
Functional: No
Inverse of: None
Description:
    Specifies a filename, as a relative path, relative to the top of the tree
    of files in the [=Repository=], of a file that existed in the previous
    version of the tree, and got removed from the tree in this [=Commit=].
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=9>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>ref</dfn>
URI: https://forgefed.org/ns#ref
Domain: [=Branch=]
Range: `xsd:string`
Functional: Yes
Inverse of: None
Description:
    Specifies an identifier for a [=Branch=], that is used in the
    [=Repository=] to uniquely refer to it. For example, in Git,
    "refs/heads/master" would be the `ref` of the master branch.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=11>
{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://forgefed.org/ns"
    ],
    "id": "https://example.dev/luke/myrepo/branches/master",
    "type": "Branch",
    "name": "master",
    "context": "https://example.dev/luke/myrepo",

    "ref": "refs/heads/master"
}
</xmp>
</div>

### Activity addressing

<pre class=simpledef>
Name: <dfn dfn export lt="prop-team">team</dfn>
URI: https://forgefed.org/ns#team
Domain: [=Object=]
Range: [=Collection=] of actors
Functional: Yes
Inverse of: None
Description:
    Specifies a [=Collection=] of actors who are working on the object, or
    responsible for it, or managing or administrating it, or having edit access
    to it. For example, for a [=Repository=], it could be the people who have
    push/edit access, the "collaborators" of the repository.
</pre>

<div class=example>
A repository *https://dev.example/aviva/treesim*:

<xmp highlight=json-ld line-highlight=20>
{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://w3id.org/security/v1",
        "https://forgefed.org/ns"
    ],
    "id": "https://dev.example/aviva/treesim",
    "type": "Repository",
    "publicKey": {
        "id": "https://dev.example/aviva/treesim#main-key",
        "owner": "https://dev.example/aviva/treesim",
        "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhki....."
    },
    "inbox": "https://dev.example/aviva/treesim/inbox",
    "outbox": "https://dev.example/aviva/treesim/outbox",
    "followers": "https://dev.example/aviva/treesim/followers",
    "name": "Tree Growth 3D Simulation",
    "summary": "<p>Tree growth 3D simulator for my nature exploration game</p>",

    "team": "https://dev.example/aviva/treesim/team"
}
</xmp>

The repository's team *https://dev.example/aviva/treesim/team*:

<xmp highlight=json-ld line-highlight=4>
{
    "@context": "https://www.w3.org/ns/activitystreams",
    "id": "https://dev.example/aviva/treesim/team",
    "type": "Collection",
    "totalItems": 3,
    "items": [
        "https://dev.example/aviva",
        "https://dev.example/luke",
        "https://code.community/users/lorax"
    ]
}
</xmp>

</div>

### Tracker linking

<pre class=simpledef>
Name: <dfn dfn export>ticketsTrackedBy</dfn>
URI: https://forgefed.org/ns#ticketsTrackedBy
Domain: [=Object=]
Range: [=Object=] that is an actor
Functional: Yes
Inverse of: [=tracksTicketsFor=]
Description:
    Identifies the actor which tracks tickets related to the given object. This
    is the actor to whom you send tickets you'd like to open against the
    object.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=10>
{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://forgefed.org/ns"
    ],
    "id": "https://dev.example/aviva/treesim",
    "type": "Repository",
    "name": "Tree Growth 3D Simulation",
    "summary": "<p>Tree growth 3D simulator for my nature exploration game</p>",
    "ticketsTrackedBy": "https://bugs.example/projects/treesim"
}
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>tracksTicketsFor</dfn>
URI: https://forgefed.org/ns#tracksTicketsFor
Domain: [=Object=] that is an actor
Range: [=Object=]
Functional: No
Inverse of: [=ticketsTrackedBy=]
Description:
    Identifies objects for which which this ticket tracker tracks tickets. When
    you'd like to open a ticket against those objects, you can send them to
    this tracker.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=8>
{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://forgefed.org/ns"
    ],
    "id": "https://bugs.example/treesim",
    "type": "Project",
    "tracksTicketsFor": [
        "https://dev.example/aviva/liblsystem",
        "https://dev.example/aviva/3d-tree-models",
        "https://dev.example/aviva/treesim"
    ]
}
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>sendPatchesTo</dfn>
URI: https://forgefed.org/ns#sendPatchesTo
Domain: [=Repository=]
Range: [=PatchTracker=]
Functional: Yes
Inverse of: [=tracksPatchesFor=]
Description:
    Identifies the [=PatchTracker=] which tracks patches and merge requests
    related to the given repository. This is the actor to whom you send patches
    and merge requests you'd like to open against the repository.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=10>
{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://forgefed.org/ns"
    ],
    "id": "https://dev.example/repos/treesim",
    "type": "Repository",
    "name": "Tree Growth 3D Simulation",
    "summary": "<p>Tree growth 3D simulator for my nature exploration game</p>",
    "sendPatchesTo": "https://bugs.example/pr-trackers/treesim"
}
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>tracksPatchesFor</dfn>
URI: https://forgefed.org/ns#tracksPatchesFor
Domain: [=PatchTracker=]
Range: [=Repository=]
Functional: No
Inverse of: [=sendPatchesTo=]
Description:
    Identifies a repository for which which this patch and merge request
    tracker tracks patches and merge requests. When you'd like to open patches
    or merge requests against that repository, you can send them to this
    tracker.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=8>
{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://forgefed.org/ns"
    ],
    "id": "https://project.example/treesim",
    "type": "PatchTracker",
    "tracksPatchesFor": [
        "https://dev.example/aviva/liblsystem",
        "https://dev.example/aviva/3d-tree-models",
        "https://dev.example/aviva/treesim"
    ]
}
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>releasesTrackedBy</dfn>
URI: https://forgefed.org/ns#releasesTrackedBy
Domain: [=Repository=]
Range: [=ReleaseTracker=]
Functional: Yes
Inverse of: [=tracksReleasesFor=]
Description:
    Identifies the actor which tracks releases for a given repository.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=10>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>tracksReleasesFor</dfn>
URI: https://forgefed.org/ns#tracksReleasesFor
Domain: [=ReleaseTracker=]
Range: [=Repository=]
Functional: No
Inverse of: [=releasesTrackedBy=]
Description:
    Identifies a repository for which this tracker tracks releases.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=8>
TODO
</xmp>
</div>

### Repository forking

<pre class=simpledef>
Name: <dfn dfn export>forkedFrom</dfn>
URI: https://forgefed.org/ns#forkedFrom
Domain: [=Repository=]
Range: [=Repository=]
Functional: Yes
Inverse of: [=forks=]
Description:
    Identifies the [=Repository=] which this [=Repository=] was created as a
    fork of, i.e. by cloning it.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=8>
{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://forgefed.org/ns"
    ],
    "id": "https://example.dev/alice/myfork/",
    "type": "Repository",
    "forkedFrom": {
        "type": "Repository",
        "id": "https://example.dev/luke/myrepo/"
    }
}
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>forks</dfn>
URI: https://forgefed.org/ns#forks
Domain: [=Repository=]
Range: [=OrderedCollection=] of items of type [=Repository=]
Functional: Yes
Inverse of: [=forkedFrom=]
Description:
    Identifies an [=OrderedCollection=] of [=Repository=]s which were created
    as forks of this [=Repository=], i.e. by cloning it. The order of the
    collection items is by reverse chronological order of the forking events.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=8>
{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://forgefed.org/ns"
    ],
    "id": "https://example.dev/luke/myrepo/",
    "type": "Repository",
    "forks": {
        "type": "OrderedCollection",
        "totalItems": 1,
        "orderedItems": [
            {
                "id": "https://example.dev/alice/myfork/",
                "type": "Repository",
            }
        ]
    },
}
</xmp>
</div>

### Access control

<pre class=simpledef>
Name: <dfn dfn export>fulfills</dfn>
URI: https://forgefed.org/ns#fulfills
Domain: [=Activity=]
Range: [=Activity=]
Functional: No
Inverse of: None
Description:
    For an activity *A* that `fulfills` some other activity *B*, specifies that
    *A* has been published as part of fulfilling the action requested by *B*.
    For example, if Alice creates a new repository using `Create`, she may want
    to instantly automatically start following this new repository using
    `Follow` (to be notified when her friends push commits there).  This
    `Follow` `fulfills` the `Create`; it's an activity automatically sent as
    part of creating a new repository.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=9>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>allows</dfn>
URI: https://forgefed.org/ns#allows
Domain: [=Grant=]
Range: [=CapabilityUsage=]
Functional: No
Inverse of: None
Description:
    Specifies which modes of using this [=Grant=] are being allowd by it. The
    two conceptual operations that `Grant`s support are invocation (acting on
    the resource under the specified role) and delegation (passing on the
    access to more actors, possibly with reduced privileges). This property
    specifies which of these operations are supported, and under which
    conditions. See [=CapabilityUsage=] for specific values to use.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=9>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>capability</dfn>
URI: https://forgefed.org/ns#capability
Domain: [=Activity=]
Range: [=Grant=]
Functional: Yes
Inverse of: None
Description:
    Specifies a previously published [=Grant=] activity providing relevant
    access permissions. For example, if Alice wants to resolve a [=Ticket=]
    under some project, she will send an activity with `capability` referring
    to the `Grant` activity that gave her collaborator access to that project,
    which happens to include permission to resolve tickets.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=9>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>publicCapability</dfn>
URI: https://forgefed.org/ns#publicCapability
Domain: [=Object=]
Range: [=Grant=]
Functional: No
Inverse of: None
Description:
    Specifies a [=Grant=] activity providing access to the [=Public=], i.e. a
    Grant that any user/actor can use to interact with the object.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=9>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>managedBy</dfn>
URI: https://forgefed.org/ns#managedBy
Domain: [=Object=]
Range: [=Object=] that is an actor
Functional: Yes
Inverse of: None
Description:
    Identifies the actor that controls the given resource, and to whom
    activities asking to modify the resource may be submitted.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=9>
{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://forgefed.org/ns"
    ],
    "type": "Ticket",
    "id": "https://example.dev/alice/myrepo/issues/42",
    "context": "https://example.dev/alice/myrepo",
    "managedBy": "https://example.dev/alice/myrepo",
    "attributedTo": "https://dev.community/bob",
    "summary": "Nothing works!",
    "content": "<p>Please fix. <i>Everything</i> is broken!</p>",
    "mediaType": "text/html",
    "source": {
        "content": "Please fix. *Everything* is broken!",
        "mediaType": "text/markdown; variant=CommonMark"
    },
    "isResolved": false
}
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>delegates</dfn>
URI: https://forgefed.org/ns#delegates
Domain: [=Grant=]
Range: [=Grant=]
Functional: Yes
Inverse of: None
Description:
    Actors can use [=Grant=] activities to allow other actors to access their
    resources. They can also allow those other actors to pass on (delegate)
    this access to even more actors. For a `Grant` that delegates access
    provided by an earlier `Grant`, the former uses `delegates` to specify the
    latter. That earlier `Grant` is also called the "parent capability" of this
    `Grant`.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=9>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>hasCollaborator</dfn>
URI: https://forgefed.org/ns#hasCollaborator
Domain: [=Actor=]
Range: [=Actor=]
Functional: No
Inverse of: None
Description:
    For a given actor *A*, specifies an actor *B* who has been giving a
    direct [=Grant=] to access/manipulate *A*
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=9>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>collaborators</dfn>
URI: https://forgefed.org/ns#collaborators
Domain: [=Actor=]
Range: A [=Collection=] of [=Relationship=] objects
Functional: Yes
Inverse of: None
Description:
    A collection of the direct collaborators of the actor, i.e. actors that
    have been given a direct-Grant to it. In each [=Relationship=] object, the
    [=subject=] is the actor, [=relationship=] is [=hasCollaborator=],
    [=object=] is the collaborator and [=instrument=] is the role.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=9>
TODO
</xmp>
</div>

### Repository mirroring

<pre class=simpledef>
Name: <dfn dfn export>mirrors</dfn>
URI: https://forgefed.org/ns#mirrors
Domain: [=Repository=]
Range: [=Repository=]
Functional: Yes
Inverse of: [=mirroredBy=]
Description:
    Identifies the [=Repository=] which this [=Repository=] copies content from
    (i.e. what this repository is a "pull mirror" of).
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=8>
{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://forgefed.org/ns"
    ],
    "id": "https://example.dev/alice/mymirror/",
    "type": "Repository",
    "mirrors": {
        "type": "Repository",
        "id": "https://example.dev/luke/myrepo/"
    }
}
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>mirroredBy</dfn>
URI: https://forgefed.org/ns#mirroredBy
Domain: [=Repository=]
Range: [=Repository=]
Functional: No
Inverse of: [=mirrors=]
Description:
    Identifies a [=Repository=] which copies content from this repository (i.e.
    "pull mirror" of this repository).
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=8>
{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://forgefed.org/ns"
    ],
    "id": "https://example.dev/luke/myrepo/",
    "type": "Repository",
    "mirroredBy": {
        "type": "Repository",
        "id": "https://example.dev/alice/mymirror/"
    }
}
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>mirrorsTo</dfn>
URI: https://forgefed.org/ns#mirrorsTo
Domain: [=Repository=]
Range: [=Repository=]
Functional: No
Inverse of: [=mirroredFrom=]
Description:
    Identifies a [=Repository=] which this repository copies content to (i.e.
    "push mirror" of this repository)
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=8>
{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://forgefed.org/ns"
    ],
    "id": "https://example.dev/alice/myrepo/",
    "type": "Repository",
    "mirrorsTo": {
        "type": "Repository",
        "id": "https://example.dev/alice-backup/myrepo/"
    }
}
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>mirroredFrom</dfn>
URI: https://forgefed.org/ns#mirroredFrom
Domain: [=Repository=]
Range: [=Repository=]
Functional: Yes
Inverse of: [=mirrorsTo=]
Description:
    Identifies the [=Repository=] which copies its content to this
    [=Repository=] (ie. what this repository is a "push mirror" of).
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=8>
{
    "@context": [
        "https://www.w3.org/ns/activitystreams",
        "https://forgefed.org/ns"
    ],
    "id": "https://example.dev/alice-backup/myrepo/",
    "type": "Repository",
    "mirroredFrom": {
        "type": "Repository",
        "id": "https://example.dev/alice/myrepo/"
    }
}
</xmp>
</div>

### Workflow enums

<pre class=simpledef>
Name: <dfn dfn export>enumValueColor</dfn>
URI: https://forgefed.org/ns#enumValueColor
Domain: [=EnumValue=]
Range:
    `xsd:string` that begins with `#` and then exactly 6 hexadecimal digits,
    representing a RGB color.
Functional: Yes
Inverse of: None
Description:
    For a given [=EnumValue=], identifies the color associated with it.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=8>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>enumValues</dfn>
URI: https://forgefed.org/ns#enumValues
Domain: [=Enum=]
Range: An [=OrderedCollection=] whose items are of type [=EnumValue=]
Functional: Yes
Inverse of: None, but [=context=] is used for that
Description:
    For a given [=Enum=], identifies the list of possible values it has.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=8>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>enumIsOrdered</dfn>
URI: https://forgefed.org/ns#enumIsOrdered
Domain: [=Enum=]
Range: `xsd:boolean`
Functional: Yes
Inverse of: None
Description:
    For a given [=Enum=], indicates whether its values have an ordering
    relation, or they're an unordered set.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=8>
TODO
</xmp>
</div>

### Workflow fields

<pre class=simpledef>
Name: <dfn dfn export lt="prop:fieldType">fieldType</dfn>
URI: https://forgefed.org/ns#fieldType
Domain: [=Field=]
Range: [=FieldType=]
Functional: Yes
Inverse of: None
Description:
    For a given [=Field=], specifies its type.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=8>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>fieldColor</dfn>
URI: https://forgefed.org/ns#fieldColor
Domain: [=Field=]
Range:
    `xsd:string` that begins with `#` and then exactly 6 hexadecimal digits,
    representing a RGB color.
Functional: Yes
Inverse of: None
Description:
    For a given [=Field=], identifies the color associated with it.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=8>
TODO
</xmp>
</div>

### Workflows

<pre class=simpledef>
Name: <dfn dfn export>workflowEnums</dfn>
URI: https://forgefed.org/ns#workflowEnums
Domain: [=Workflow=]
Range: [=OrderedCollection=], ordered by publishing time, of [=Enum=] objects
Functional: Yes
Inverse of: None, but [=context=] is used for that
Description:
    For a given [=Workflow=], specifies its set of enum types.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=8>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>workflowFields</dfn>
URI: https://forgefed.org/ns#workflowFields
Domain: [=Workflow=]
Range: [=OrderedCollection=] of [=Field=] objects
Functional: Yes
Inverse of: None, but [=context=] is used for that
Description:
    For a given [=Workflow=], specifies its set of custom fields.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=8>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>workflowTrackers</dfn>
URI: https://forgefed.org/ns#workflowTrackers
Domain: [=Workflow=]
Range: [=OrderedCollection=] of [=TicketTracker=] and [=PatchTracker=] objects
Functional: Yes
Inverse of: None, but see [=trackerWorkflow=]
Description:
    For a given [=Workflow=], specifies the trackers it is serving.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=8>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>trackerWorkflow</dfn>
URI: https://forgefed.org/ns#trackerWorkflow
Domain: [=TicketTracker=] | [=PatchTracker=]
Range: [=Workflow=]
Functional: Yes
Inverse of: None, but see [=workflowTrackers=]
Description:
    For a given tracker, specifies the workflow it uses for custom fields.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=8>
TODO
</xmp>
</div>

### Milestones

<pre class=simpledef>
Name: <dfn dfn export>roadmapMilestones</dfn>
URI: https://forgefed.org/ns#roadmapMilestones
Domain: [=Workflow=]
Range:
    [=OrderedCollection=], ordered by publishing time, of [=Milestones=]
    objects
Functional: Yes
Inverse of: None, but [=context=] is used for that
Description:
    For a given [=Roadmap=], specifies its set of milestones.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=8>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>roadmapTrackers</dfn>
URI: https://forgefed.org/ns#roadmapTrackers
Domain: [=Roadmap=]
Range: [=OrderedCollection=] of [=TicketTracker=] and [=PatchTracker=] objects
Functional: Yes
Inverse of: None, but see [=trackerRoadmap=]
Description:
    For a given [=Roadmap=], specifies the trackers it is serving.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=8>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>trackerRoadmap</dfn>
URI: https://forgefed.org/ns#trackerRoadmap
Domain: [=TicketTracker=] | [=PatchTracker=]
Range: [=Roadmap=]
Functional: Yes
Inverse of: None, but see [=roadmapTrackers=]
Description:
    For a given tracker, specifies the roadmap it uses for milestones.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=8>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>ticketMilestone</dfn>
URI: https://forgefed.org/ns#ticketMilestone
Domain: [=Ticket=]
Range: [=Milestone=]
Functional: Yes
Inverse of: None, but see [=milestoneTickets=]
Description:
    For a given ticket, specifies the milestone it belongs to.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=8>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>milestoneTickets</dfn>
URI: https://forgefed.org/ns#milestoneTickets
Domain: [=Milestone=]
Range: [=Collection=] of [=Ticket=]s
Functional: Yes
Inverse of: None, but see [=ticketMilestone=]
Description:
    For a given milestone, specifies the tickets that belong to it.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=8>
TODO
</xmp>
</div>

### Releases

<pre class=simpledef>
Name: <dfn dfn export>isPreRelease</dfn>
URI: https://forgefed.org/ns#isPreRelease
Domain: [=Release=]
Range: `xsd:boolean`
Functional: Yes
Inverse of: None
Description:
    Whether this is a pre-release.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=8>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>releaseMilestones</dfn>
URI: https://forgefed.org/ns#releaseMilestones
Domain: [=Release=]
Range: [=Collection=] of [=Milestone=]s
Functional: Yes
Inverse of: None, but see [=milestoneReleases=]
Description:
    For a given release, specifies the milestones it belongs to.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=8>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>milestoneReleases</dfn>
URI: https://forgefed.org/ns#milestoneReleases
Domain: [=Milestone=]
Range: [=Collection=] of [=Release=]s
Functional: Yes
Inverse of: None, but see [=releaseMilestones=]
Description:
    For a given milestone, specifies the releases associated with it.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=8>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>releases</dfn>
URI: https://forgefed.org/ns#releases
Domain: [=ReleaseTracker=]
Range: [=Collection=] of [=Release=]s
Functional: Yes
Inverse of: None, but see [=context=] in [[#model-release]]
Description:
    Specify the releases tracked by a release tracker.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=8>
TODO
</xmp>
</div>

### Merge Requests

<pre class=simpledef>
Name: <dfn dfn export>isWip</dfn>
URI: https://forgefed.org/ns#isWip
Domain: [=Offer=]
Range: `xsd:boolean`
Functional: Yes
Inverse of: None
Description:
    Whether the Merge Request is considered work-in-progress and thus not ready
    for merging.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=8>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>mrDiff</dfn>
URI: https://forgefed.org/ns#mrDiff
Domain: [=Offer=]
Range: [=Link=]
Functional: Yes
Inverse of: None
Description:
    The [=Link=]'s [=href=] points to a unified diff, without commit
    information, that contains all the changes in the Merge Request merged
    together (unlike the [=Collection=] of [=Patch=]es where each commit's
    changes are kept separately)
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=8>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>verdict</dfn>
URI: https://forgefed.org/ns#verdict
Domain: [=Review=]
Range: [=ReviewVerdict=]
Functional: Yes
Inverse of: None
Description:
    The level of approval this review gives on the merge request.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=8>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>reviewIsBinding</dfn>
URI: https://forgefed.org/ns#reviewIsBinding
Domain: [=Review=] | [=Approval=]
Range: `xsd:boolean`
Functional: Yes
Inverse of: None
Description:
    Whether the review is made by someone with [=write=] or higher access to
    the [=PatchTracker=]
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=8>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>cqFile</dfn>
URI: https://forgefed.org/ns#cqFile
Domain: [=CodeQuote=]
Range: `xsd:string`
Functional: Yes
Inverse of: None
Description:
    Path, relative to repo root, of the file whose changes are being quoted
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=8>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>cqHunk</dfn>
URI: https://forgefed.org/ns#cqHunk
Domain: [=CodeQuote=]
Range: `xsd:string`
Functional: Yes
Inverse of: None
Description:
    A diff hunk from the merge request's diff
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=8>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>cqLine</dfn>
URI: https://forgefed.org/ns#cqLine
Domain: [=CodeQuote=]
Range: `xsd:integer`
Functional: Yes
Inverse of: None
Description:
    The line of code being commented. It may refer to the old or new version of
    the file, depending on [=cqSide=].
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=8>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>cqStart</dfn>
URI: https://forgefed.org/ns#cqStart
Domain: [=CodeQuote=]
Range: `xsd:integer`
Functional: Yes
Inverse of: None
Description:
    The first line in a range of lines of code being commented. It may refer to
    the old or new version of the file, depending on [=cqStartSide=].
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=8>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>cqEnd</dfn>
URI: https://forgefed.org/ns#cqEnd
Domain: [=CodeQuote=]
Range: `xsd:integer`
Functional: Yes
Inverse of: None
Description:
    The last line in a range of lines of code being commented. It may refer to
    the old or new version of the file, depending on [=cqEndSide=].
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=8>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>cqSide</dfn>
URI: https://forgefed.org/ns#cqSide
Domain: [=CodeQuote=]
Range: [=DiffSide=]
Functional: Yes
Inverse of: None
Description:
    To which version of the file [=cqLine=] refers.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=8>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>cqStartSide</dfn>
URI: https://forgefed.org/ns#cqStartSide
Domain: [=CodeQuote=]
Range: [=DiffSide=]
Functional: Yes
Inverse of: None
Description:
    To which version of the file [=cqStart=] refers.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=8>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>cqEndSide</dfn>
URI: https://forgefed.org/ns#cqEndSide
Domain: [=CodeQuote=]
Range: [=DiffSide=]
Functional: Yes
Inverse of: None
Description:
    To which version of the file [=cqEnd=] refers.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=8>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>cqOutdated</dfn>
URI: https://forgefed.org/ns#cqOutdated
Domain: [=CodeQuote=]
Range: `xsd:boolean`
Functional: Yes
Inverse of: None
Description:
    Whether these line(s) have been changed by a later version of the MR
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=8>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>reviewThreads</dfn>
URI: https://forgefed.org/ns#reviewThreads
Domain: [=Review=]
Range: [=ReviewThread=]
Functional: No
Inverse of: None
Description:
    Comments on code and their discussion
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=8>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>approvalStatus</dfn>
URI: https://forgefed.org/ns#approvalStatus
Domain: [=Approval=]
Range: [=ReviewStatus=]
Functional: Yes
Inverse of: None
Description:
    The status of the approval
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=8>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>reviewIsRequested</dfn>
URI: https://forgefed.org/ns#reviewIsRequested
Domain: [=Approval=]
Range: `xsd:boolean`
Functional: Yes
Inverse of: None
Description:
    Whether a review from this person/team was specifically personally
    requested/invited, or was is given without a personal request
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=8>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>reviewIsByCodeOwner</dfn>
URI: https://forgefed.org/ns#reviewIsByCodeOwner
Domain: [=Approval=]
Range: `xsd:boolean`
Functional: Yes
Inverse of: None
Description:
    Whether this review/request is here because (perhaps among other reasons)
    this person/team is a Code Owner of some of the files being changed by the
    merge request
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=8>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>reviewCountsInRule</dfn>
URI: https://forgefed.org/ns#reviewCountsInRule
Domain: [=Approval=]
Range: `xsd:boolean`
Functional: Yes
Inverse of: None
Description:
    Whether this approval count towards required amount of approvals under some
    rule
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=8>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>newReviewInProgress</dfn>
URI: https://forgefed.org/ns#newReviewInProgress
Domain: [=Approval=]
Range: `xsd:boolean`
Functional: Yes
Inverse of: None
Description:
    Whether the person has indicated they're in the process of making a new
    review
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=8>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>individualApprovals</dfn>
URI: https://forgefed.org/ns#individualApprovals
Domain: [=Offer=]
Range: [=OrderedCollection=] of [=Approval=]s, in reverse chronological order
Functional: Yes
Inverse of: None
Description:
    Approval statuses by individuals.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=8>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>teamApprovals</dfn>
URI: https://forgefed.org/ns#teamApprovals
Domain: [=Offer=]
Range: [=OrderedCollection=] of [=Approval=]s, in reverse chronological order
Functional: Yes
Inverse of: None
Description:
    Approval statuses by [=Team=]s.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=8>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>requiredApprovalsNeeded</dfn>
URI: https://forgefed.org/ns#requiredApprovalsNeeded
Domain: [=Offer=]
Range: `xsd:integer`
Functional: Yes
Inverse of: None
Description:
    Total number of approvals required before the MR can be merged.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=8>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>requiredApprovalsGiven</dfn>
URI: https://forgefed.org/ns#requiredApprovalsGiven
Domain: [=Offer=]
Range: `xsd:integer`
Functional: Yes
Inverse of: None
Description:
    Amount of required approvals given, out of the amount needed before the MR
    can be merged.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=8>
TODO
</xmp>
</div>

### Organizations

<pre class=simpledef>
Name: <dfn dfn export>hasMember</dfn>
URI: https://forgefed.org/ns#hasMember
Domain: [=Organization=]
Range: [=Person=]
Functional: No
Inverse of: None
Description:
    Identifier a [=Person=] who is a member of this [=Organization=].
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=9>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>members</dfn>
URI: https://forgefed.org/ns#members
Domain: [=Organization=]
Range:
    [=Collection=] of [=Relationship=]s whose [=relationship=] is [=hasMember=]
    and whose [=subject=] is this `Organization`.
Functional: Yes
Inverse of: None
Description:
    Identifies a collection of the members of this [=Organization=],
    represented as [=hasMember=] [=Relationship=]s.
</pre>

<pre class=simpledef>
Name: <dfn dfn export>orgAssets</dfn>
URI: https://forgefed.org/ns#orgAssets
Domain: [=Organization=]
Range:
    [=Collection=] of [=Relationship=]s whose [=relationship=] is [=hasAsset=]
    and whose [=subject=] is this `Organization`.
Functional: Yes
Inverse of: None
Description:
    Projects considered as being developed under the organization.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=8>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>hasAsset</dfn>
URI: https://forgefed.org/ns#hasAsset
Domain: [=Organization=]
Range: [=Project=] | component
Functional: No
Inverse of: None
Description:
    Identifies a project or component that belongs to the organization
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=9>
TODO
</xmp>
</div>

<pre class=simpledef>
Name: <dfn dfn export>organizations</dfn>
URI: https://forgefed.org/ns#organizations
Domain: [=Project=] | components
Range:
    [=Collection=] of [=Relationship=]s whose [=relationship=] is [=hasAsset=]
    and whose [=subject=] is this `Organization`.
Functional: Yes
Inverse of: None, but see [=orgAssets=]
Description:
    Organizations considered as managing this project.
</pre>

<div class=example>
<xmp highlight=json-ld line-highlight=8>
TODO
</xmp>
</div>

## Values

### Capability uses

<pre class=simpledef>
Name: <dfn dfn export>gatherAndConvey</dfn>
URI: https://forgefed.org/ns#gatherAndConvey
Type: [=CapabilityUsage=]
Conditions:
    <ul>
        <li>
            The `Grant`'s [=target=] MUST be a [=Project=]
        </li>
        <li>
            It may delegate the `Grant`, allowing only `gatherAndConvey`, to
            parent projects
        </li>
        <li>
            It may delegate the `Grant`, allowing only `distribute`, to teams
            to which it allows to access it
        </li>
        <li>
            It may delegate the `Grant`, allowing `invoke` only, to people to
            which it allows to access it
        </li>
    </ul>
</pre>

<pre class=simpledef>
Name: <dfn dfn export>distribute</dfn>
URI: https://forgefed.org/ns#distribute
Type: [=CapabilityUsage=]
Conditions:
    <ul>
        <li>
            The `Grant`'s [=target=] MUST be a [=Team=]
        </li>
        <li>
            It may delegate the `Grant`, allowing `distribute` only, to its
            subteams
        </li>
        <li>
            It may delegate the `Grant`, allowing `invoke` only, to its members
        </li>
    </ul>
</pre>

<pre class=simpledef>
Name: <dfn dfn export>invoke</dfn>
URI: https://forgefed.org/ns#invoke
Type: [=CapabilityUsage=]
Conditions:
    <ul>
        <li>
            The `Grant`'s [=target=] may invoke it, i.e. use it as the
            [=capability=] in another activity, that requests to access or
            modify the resource specified by the `Grant`'s [=context=]
        </li>
    </ul>
</pre>

### Roles

<pre class=simpledef>
Name: <dfn dfn export>visit</dfn>
URI: https://forgefed.org/ns#visit
Type: [=Role=]
Description:
    Authorizes the [=Grant=] recipient (i.e. [=target=]) to view the [=Grant=]
    resource (i.e. [=context=]), which includes retrieving objects via HTTP and
    pulling/cloning VCS repos
</pre>

<pre class=simpledef>
Name: <dfn dfn export>report</dfn>
URI: https://forgefed.org/ns#report
Type: [=Role=]
Description:
    Authorizes the [=Grant=] recipient (i.e. [=target=]) to do on the [=Grant=]
    resource (i.e. [=context=]) anything that the [=visit=] role authorizes,
    and also to do basic community participation tasks:
    [Open an issue](#opening-issue),
    [submit a PR](#opening-mr),
    [create comments](#commenting)
    and discussion threads,
    edit public wikis,
    submit PR reviews.
</pre>

<pre class=simpledef>
Name: <dfn dfn export>triage</dfn>
URI: https://forgefed.org/ns#triage
Type: [=Role=]
Description:
    Authorizes the [=Grant=] recipient (i.e. [=target=]) to do on the [=Grant=]
    resource (i.e. [=context=]) anything that the [=report=] role authorizes,
    and also to edit issue/PR propeties (labels, milestones, due dates, etc.),
    close and reopen issues and PRs, assign and unassign people to issues and
    PRs, request PR reviews, hide disruptive comments (a moderation action),
    lock and move discussions.
</pre>

<pre class=simpledef>
Name: <dfn dfn export>write</dfn>
URI: https://forgefed.org/ns#write
Type: [=Role=]
Description:
    Authorizes the [=Grant=] recipient (i.e. [=target=]) to do on the [=Grant=]
    resource (i.e. [=context=]) anything that the [=triage=] role authorizes,
    and also to
    apply PR suggested changes,
    edit non-public wikis,
    create/edit/delete labels,
    merge a PR,
    [push to VCS repositories](#pushing),
    create/edit/run/cancel CI recipes,
    manage releases,
    publish packages,
    create web IDE coding sessions.
</pre>

<pre class=simpledef>
Name: <dfn dfn export>maintain</dfn>
URI: https://forgefed.org/ns#maintain
Type: [=Role=]
Description:
    Authorizes the [=Grant=] recipient (i.e. [=target=]) to do on the [=Grant=]
    resource (i.e. [=context=]) anything that the [=write=] role authorizes,
    and also to edit project and component descriptions and settings unrelated
    to access, enable/disable components, configure "Pages" publishing of
    static websites from repos, push to repos' protected branches.
</pre>

<pre class=simpledef>
Name: <dfn dfn export>admin</dfn>
URI: https://forgefed.org/ns#admin
Type: [=Role=]
Description:
    Authorizes the [=Grant=] recipient (i.e. [=target=]) to do on the [=Grant=]
    resource (i.e. [=context=]) anything that the [=maintain=] role authorizes,
    and also to
    [manage access to projects, components and teams](#managing-access),
    merge PRs even without reviews,
    delete issues,
    change project/component/team visibility,
    edit project/component/team access-related settings,
    change a repo's default branch,
    manage webhooks and deployment,
    move components and projects between projects,
    archive projects/components,
    delete components/projects/teams.
</pre>

<pre class=simpledef>
Name: <dfn dfn export>delegate</dfn>
URI: https://forgefed.org/ns#delegate
Type: [=Role=]
Description:
    Authorizes the [=Grant=] recipient (i.e. [=target=]) to send access
    delegations to the [=Grant=] sender (i.e. [=actor=])
</pre>

### Field Types

<pre class=simpledef>
Name: <dfn dfn export>fieldTypeText</dfn>
URI: https://forgefed.org/ns#fieldTypeText
Type: [=FieldType=]
Description:
    Text. Any JSON text string. Example: `"hello"`.
</pre>

<pre class=simpledef>
Name: <dfn dfn export>fieldTypeInteger</dfn>
URI: https://forgefed.org/ns#fieldTypeInteger
Type: [=FieldType=]
Description:
    Any integral numeric literal. Example: `42`.
</pre>

<pre class=simpledef>
Name: <dfn dfn export>fieldTypeRational</dfn>
URI: https://forgefed.org/ns#fieldTypeRational
Type: [=FieldType=]
Description:
    Any numeric literal. Example: `3.14`.
</pre>

<pre class=simpledef>
Name: <dfn dfn export>fieldTypeBoolean</dfn>
URI: https://forgefed.org/ns#fieldTypeBoolean
Type: [=FieldType=]
Description:
    A boolean value. Examples: `true`, `false`.
</pre>

<pre class=simpledef>
Name: <dfn dfn export>fieldTypeClass</dfn>
URI: https://forgefed.org/ns#fieldTypeClass
Type: [=FieldType=]
Description:
    No value. The presence or absence of the field itself is the information.
    Can be used for representing simple issue labels.
</pre>

<pre class=simpledef>
Name: <dfn dfn export>fieldTypeEnum</dfn>
URI: https://forgefed.org/ns#fieldTypeEnum
Type: [=FieldType=]
Description:
    A certain specific [=Enum=].
</pre>

### Review Statuses

<pre class=simpledef>
Name: <dfn dfn export>approve</dfn>
URI: https://forgefed.org/ns#approve
Type: [=ReviewVerdict=]
Description:
    The reviewer is approving the merge request.
</pre>

<pre class=simpledef>
Name: <dfn dfn export>remark</dfn>
URI: https://forgefed.org/ns#remark
Type: [=ReviewVerdict=]
Description:
    The reviewer is commenting on the merge request, without approving and
    without requiring changes. Proposing changes but willing for the merge
    request to be merged as is.
</pre>

<pre class=simpledef>
Name: <dfn dfn export>revise</dfn>
URI: https://forgefed.org/ns#revise
Type: [=ReviewVerdict=]
Description:
    The reviewer is requesting/requiring that changes be made in the merge
    request before it is merged. The merge request as it is now isn't approved.
</pre>

<pre class=simpledef>
Name: <dfn dfn export>awaiting</dfn>
URI: https://forgefed.org/ns#awaiting
Type: [=ReviewStatus=]
Description:
    No review by this reviewer has been given yet.
</pre>

<pre class=simpledef>
Name: <dfn dfn export>dismissed</dfn>
URI: https://forgefed.org/ns#dismissed
Type: [=ReviewStatus=]
Description:
    The review has been dismissed and doesn't count as an approval anymore.
</pre>

<pre class=simpledef>
Name: <dfn dfn export>dismissedAwaiting</dfn>
URI: https://forgefed.org/ns#dismissedAwaiting
Type: [=ReviewStatus=]
Description:
    The review has been dismissed, and now waiting for a new review.
</pre>

<pre class=simpledef>
Name: <dfn dfn export>diffSideOld</dfn>
URI: https://forgefed.org/ns#diffSideOld
Type: [=DiffSide=]
Description:
    Refers to the original version of the file.
</pre>

<pre class=simpledef>
Name: <dfn dfn export>diffSideNew</dfn>
URI: https://forgefed.org/ns#diffSideNew
Type: [=DiffSide=]
Description:
    Refers to the modified version of the file.
</pre>

<pre class=biblio>
{
    "wikipedia-ticket-tracker": {
        "href": "https://en.wikipedia.org/wiki/Issue_tracking_system",
        "title": "Wikipedia: Issue tracking system"
    },
    "wikipedia-pr": {
        "href": "https://en.wikipedia.org/wiki/Distributed_version_control#Pull_requests",
        "title": "Wikipedia: Pull requests"
    },
    "fep-8b32": {
        "href": "https://codeberg.org/fediverse/fep/src/branch/main/feps/fep-8b32.md",
        "title": "FEP-8b32: Object Integrity Proofs"
    }
}
</pre>

<pre class=anchors>
urlPrefix: https://www.w3.org/TR/xmlschema11-2/#; type: dfn; spec: xmlschema11-2
    text: dateTime
urlPrefix: http://purl.org/dc/terms/; type: dfn; spec: dcterms
    text: created
urlPrefix: https://www.w3.org/TR/activitystreams-vocabulary/#dfn-; type: dfn; spec: activitystreams-vocabulary
    text: Accept
    text: Add
    text: Announce
    text: Create
    text: Invite
    text: Join
    text: Leave
    text: Like
    text: Move
    text: Offer
    text: Reject
    text: Remove
    text: Undo
    text: Update
urlPrefix: https://www.w3.org/TR/activitystreams-vocabulary/#dfn-; type: dfn; spec: activitystreams-vocabulary
    text: Activity
    text: Collection
    text: Image
    text: Link
    text: Note
    text: Object
    text: OrderedCollection
    text: Organization
    text: Person
urlPrefix: https://www.w3.org/TR/activitystreams-vocabulary/#dfn-; type: dfn; spec: activitystreams-vocabulary
    text: actor
    text: attachment
    text: attributedTo
    text: content
    text: context
    text: duration
    text: endTime
    text: href
    text: id
    text: inbox
    text: inReplyTo
    text: instrument
    text: items
    text: mediaType
    text: name
    text: object
    text: orderedItems
    text: origin
    text: published
    text: relationship
    text: replies
    text: result
    text: source
    text: startTime
    text: subject
    text: summary
    text: tag
    text: target
    text: to
    text: totalItems
    text: type
urlPrefix: https://www.w3.org/TR/activitypub/#; type: dfn; spec: activitypub
    text: Public; url: https://www.w3.org/TR/activitypub/#public-addressing
    text: followers
    text: following
    text: likes
</pre>
